Frank
Frank

Reputation: 6637

Jackson JSON JsonTypeInfo and JsonSubTypes Integer value is serialized as String to JSON

In my "quest" to parse lottieFile and dotLottie JSONs into Java objects and back to identical lottieFiles (see https://lottie4j.com/), I discovered a strange effect of @JsonSubTypes.

Java 17 + Jackson 2.14.1

As I'm making an implementation on top of the Lottie data model, it's not possible to make a change in the JSON format.

It seems that the following code, causes the Integer value of a to become a String when converting back to JSON.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "a")
@JsonSubTypes({
      @JsonSubTypes.Type(names = {"0", "0.0"}, value = FixedBezier.class),
      @JsonSubTypes.Type(names = {"1", "1.0"}, value = AnimatedBezier.class)
})

This is the test result:

Original:     {"a": 0, "d": "test"}
Generated:    {"a": "0", "d": "test"}

Even when using

@JsonSerialize(using = IntegerSerializer.class)

or

@JsonSerialize(using = AnimatedSerializer.class)

With or without these @JsonSerialize, "a": "0" is generated instead of "a": 0. Am I missing some configuration to force the JsonSubType value to be stored as a number in the JSON? How can this be achieved?

This is my full test code, with both the @JsonSerialize commented to easily switch between different possible solutions...

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.io.IOException;

public class BezierTest {

    private static final ObjectMapper mapper = new ObjectMapper();

    public static void main(String[] args) {
        String[] jsons = new String[]{
                // jsonLottieFilesFixed
                "{\"a\":0,\"d\":\"test\"}",
                // jsonLottieFilesAnimated
                "{\"a\":1,\"d\":\"test\"}",
                // dotLottieFilesFixed
                "{\"a\":0.0,\"d\":\"test\"}",
                // dotLottieFilesAnimated
                "{\"a\":1.0,\"d\":\"test\"}"
        };
        for (String json : jsons) {
            test(json);
        }
    }

    private static void test(String json) {
        BaseBezier objectFromJson = null;
        try {
            objectFromJson = mapper.readValue(json, BaseBezier.class);
            ObjectMapper mapper = new ObjectMapper();
            String jsonFromObject = mapper.writeValueAsString(objectFromJson);

            System.out.println("Original:\t" + json);
            System.out.println("Generated:\t" + jsonFromObject);

            System.out.println("Is equal: " + json.equals(jsonFromObject));
        } catch (JsonProcessingException e) {
            System.err.println("Json exception: " + e.getMessage());
        }
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "a")
    @JsonSubTypes({
            @JsonSubTypes.Type(names = {"0", "0.0"}, value = FixedBezier.class),
            @JsonSubTypes.Type(names = {"1", "1.0"}, value = AnimatedBezier.class)
    })
    interface BaseBezier {

    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @JsonInclude(JsonInclude.Include.NON_NULL)
    record FixedBezier(
            @JsonProperty("a")
            @JsonSerialize(using = AnimatedSerializer.class)
            Integer animated,

            @JsonProperty("d")
            String data

    ) implements BaseBezier {
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public record AnimatedBezier(
            @JsonProperty("a")
            // @JsonSerialize(using = AnimatedSerializer.class)
            Integer animated,

            @JsonProperty("d")
            String data
    ) implements BaseBezier {
    }

    private static class AnimatedSerializer extends JsonSerializer {
        @Override
        public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeNumber((int) o);
        }
    }
}

Upvotes: 1

Views: 777

Answers (1)

jon hanson
jon hanson

Reputation: 9408

The problem is that you're using JSON field "a" as both a type identifier:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "a")

and as a field identifier:

@JsonProperty("a")

Since the type identifiers have to be string values, this is what you're seeing in the resultant JSON - the type id expressed as a string.

If you change one of them to use a different field name, and update your test JSON accordingly, then it works, e.g.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "id")

// jsonLottieFilesFixed
"""
{ "id": "0", "a": 0, "d": "test"}
""",

with the following output:

Original:
{ "id": "0", "a": 0, "d": "test"}

Generated:
{"id":"0","a":0,"d":"test"}

Upvotes: 2

Related Questions