I'm working on a Java library to be able to read/write Lottie data (animations defined as JSON). I try to achieve this with minimal code and Records, but it's a bit challenging to achieve this for all possible use cases as defined in the Lottie format. I've been able to handle almost everything, but I still need to find a suitable solution for the keyframes.
Given the following unit tests, how should the Java objects be defined to be able to parse the example JSONs? Is this possible with "pure Jackson" or will a helper class be needed? I'm using Jackson 2.14.1.
At this point, only testTimed
succeeds.
public class KeyframeTest {
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testInteger() throws JsonProcessingException {
var json = """
{
"k": [
128,
256
]
}
""";
var animated = mapper.readValue(json, Animated.class);
assertAll(
() -> assertEquals(2, animated.keyframes().size()),
() -> assertTrue(animated.keyframes().get(0) instanceof NumberKeyframe),
() -> assertEquals(128, animated.keyframes().get(0)),
() -> JSONAssert.assertEquals(json, mapper.writeValueAsString(animated), false)
);
}
@Test
void testDouble() throws JsonProcessingException {
var json = """
{
"k": [
5.01,
6.02
]
}
""";
var animated = mapper.readValue(json, Animated.class);
assertAll(
() -> assertEquals(2, animated.keyframes().size()),
() -> assertTrue(animated.keyframes().get(0) instanceof NumberKeyframe),
() -> assertEquals(5.01, animated.keyframes().get(0)),
() -> JSONAssert.assertEquals(json, mapper.writeValueAsString(animated), false)
);
}
@Test
void testTimed() throws JsonProcessingException {
var json = """
{
"k": [
{
"i": {
"x": [
0.833
],
"y": [
0.833
]
},
"o": {
"x": [
0.167
],
"y": [
0.167
]
},
"t": 60,
"s": [
1.1,
2.2,
3.3
]
},
{
"t": 60,
"s": [
360.0
]
}
]
}
""";
var animated = mapper.readValue(json, Animated.class);
assertAll(
() -> assertEquals(2, animated.keyframes().size()),
() -> assertTrue(animated.keyframes().get(0) instanceof TimedKeyframe),
() -> assertEquals(60, ((TimedKeyframe) animated.keyframes().get(0)).time()),
() -> JSONAssert.assertEquals(json, mapper.writeValueAsString(animated), false)
);
}
@Test
void testMixed() throws JsonProcessingException {
var json = """
{
"k": [
100,
33.44,
{
"t": 60,
"s": [
1.1,
2.2,
3.3
]
}
]
}
""";
var keyFrames = mapper.readValue(json, new TypeReference<List<Keyframe>>() {
});
assertAll(
() -> assertEquals(3, keyFrames.size()),
() -> assertTrue(keyFrames.get(0) instanceof NumberKeyframe),
() -> assertTrue(keyFrames.get(1) instanceof NumberKeyframe),
() -> assertTrue(keyFrames.get(2) instanceof TimedKeyframe),
() -> JSONAssert.assertEquals(json, mapper.writeValueAsString(keyFrames), false)
);
}
}
Animated object
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(Include.NON_NULL)
public record Animated(
@JsonProperty("a") Integer animated,
@JsonProperty("k") List<Keyframe> keyframes,
@JsonProperty("ix") Integer ix,
@JsonProperty("l") Integer l
) {
}
Keyframe objects, using Java Records based on my earlier question here Parse JSON to Java records with fasterxml.jackson
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(TimedKeyframe.class),
@JsonSubTypes.Type(NumberKeyframe.class)
})
public interface Keyframe {
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record NumberKeyframe(
BigDecimal value
) implements Keyframe {
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record TimedKeyframe(
@JsonProperty("t") Integer time, // in frames
// Use BigDecimal here to be able to handle both Integer and Double
// https://stackoverflow.com/questions/40885065/jackson-mapper-integer-from-json-parsed-as-double-with-drong-precision
@JsonProperty("s") List<BigDecimal> values,
@JsonProperty("i") EasingHandle easingIn,
@JsonProperty("o") EasingHandle easingOut,
@JsonProperty("h") Integer holdFrame
) implements Keyframe {
}
This is the failure message for testDouble:
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class com.lottie4j.core.model.keyframe.Keyframe]: Unexpected input
at [Source: (String)"{
"k": [
5.01,
6.02
]
}
"; line: 3, column: 7] (through reference chain: com.lottie4j.core.model.Animated["k"]->java.util.ArrayList[0])
CodePudding user response:
Looks like jackson has a problem with deserializing numbers into objects. You could solve this using a custom deserializer or by making your NumberKeyFrame extend BigDecimal instead. Here is a working minimal example but I removed a lot of your code. Notice the defaultImpl in the JsonTypeInfo annotaton. This was necessary in order to work though I'm not 100% why :see_no_evil:
public class MyTest {
@JsonIgnoreProperties(ignoreUnknown = true)
static class Animated {
public @JsonProperty("k") List<Keyframe> keyframes = new ArrayList<>();
}
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, defaultImpl = NumberKeyframe.class)
@JsonSubTypes({
@JsonSubTypes.Type(NumberKeyframe.class),
@JsonSubTypes.Type(TimedKeyframe.class),
})
public interface Keyframe {
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class NumberKeyframe extends BigDecimal implements Keyframe {
public NumberKeyframe(int val) {
super(val);
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class TimedKeyframe implements Keyframe {
@JsonProperty("s")
List<BigDecimal> values;
}
private static final ObjectMapper mapper = new ObjectMapper();
@Test
public void testInteger() throws JsonProcessingException, JSONException {
var json = "{\"k\": [128,256]}";
var animated = mapper.readValue(json, Animated.class);
assertEquals(2, animated.keyframes.size());
assertTrue(animated.keyframes.get(0) instanceof NumberKeyframe);
assertEquals(128, ((NumberKeyframe) animated.keyframes.get(0)).intValue());
}
@Test
void testTimed() throws JsonProcessingException {
var json = "{\"k\": [\n"
" {\"i\": {\"x\": [0.833],\"y\": [0.833]},\"o\": {\"x\": [0.167],\"y\": [0.167]},\"t\": 60,\"s\": [1.1,2.2,3.3]},\n"
" { \"t\": 60, \"s\": [360.0]}\n"
"]}";
var animated = mapper.readValue(json, Animated.class);
assertEquals(2, animated.keyframes.size());
assertTrue(animated.keyframes.get(0) instanceof TimedKeyframe);
assertEquals(1.1, ((TimedKeyframe) animated.keyframes.get(0)).values.get(0).doubleValue());
assertEquals(2.2, ((TimedKeyframe) animated.keyframes.get(0)).values.get(1).doubleValue());
assertEquals(3.3, ((TimedKeyframe) animated.keyframes.get(0)).values.get(2).doubleValue());
}
}
CodePudding user response:
The mapping seems incorrect. What you have is this:
var json = """
{
"k": [
5.01,
6.02
]
}
""";
This is effectively an array of numbers which I assume should map to NumberKeyframe
. But that would map to something like:
"k": [{"value": 5.01}, {"value": 6.01}]
I'm not sure if this is something Jackson can do seamlessly without a mapper.