FasterXML / jackson-dataformat-xml

Extension for Jackson JSON processor that adds support for serializing POJOs as XML (and deserializing from XML) as an alternative to JSON
Apache License 2.0
561 stars 221 forks source link

`@JsonDeserialize` prioritized over `addDeserializer` #592

Closed JanStureNielsen closed 1 year ago

JanStureNielsen commented 1 year ago

Using Jackson Databind 2.13.5, annotating a POJO with @JsonDeserialize:

@Getter
@Builder
@ToString
@JsonDeserialize(builder=Person.PersonBuilder.class)
static class Person {
    private final String name;
    private final List<Pet> pets;

}

@Getter
@Builder
@ToString
static class Pet {
    private final String name;
    private final String type;

}

takes priority over an explicitly configured deserializer in a mapper:

String serializedPersons = """
name,pets
Ali,"[{'Cutesy','cat'}, {'Tootsie','cat'}]"
Jan,"[{'Mr. Bubbles','dog'}, {'Lilly','cat'}]"
Tom,"[{'Fester','dog'}, {'Wednesday','cat'}]"
Zoe,"[{'Jaeger','dog'}, {'Deira','cat'}]"
""";

CsvMapper mapper = new CsvMapper();
SimpleModule module = new SimpleModule();

module.addSerializer(Person.class, new PersonSerializer());
module.addDeserializer(Person.class, new PersonDeserializer());

mapper.registerModule(module);

CsvSchema schema = mapper.schemaFor(Person.class).withHeader();
ObjectReader reader = mapper.readerFor(Person.class).with(schema);

MappingIterator<Person> i = reader.readValues(serializedPersons);

while (i.hasNext()) {
    System.out.println("Person: " + i.next());
}

It seems like explicitly configured serializers & deserializers on the mappers should take precedence over others that are discovered.

In my case, I have network marshalled objects that should be using annotated builder, and CLI applications that should be using their own explicitly configured deserializers. I wrote a question on SO before I found the problem.


public class JacksonTest {
    @Test
    public void writeJson() throws JsonProcessingException {
        var mapper = new ObjectMapper();
        var writer = mapper.writer();

        System.out.println(writer.writeValueAsString(_single()));
        System.out.println(writer.writeValueAsString(_multiple()));
    }

    @Test
    public void writeJsonObjectsIndividually() throws IOException {
        var mapper = new ObjectMapper();
        var writer = new StringWriter();

        try (var seqWriter = mapper.writerFor(Person.class).writeValues(writer)) {
            seqWriter.write(_single());
            seqWriter.writeAll(_multiple());
        }
        System.out.println(writer.toString());
    }

    @Test
    public void writePersonToCsv() throws IOException {
        CsvMapper mapper = new CsvMapper();
        SimpleModule module = new SimpleModule();

        module.addSerializer(Person.class, new PersonSerializer());
        module.addDeserializer(Person.class, new PersonDeserializer());

        mapper.registerModule(module);
        var writer = new StringWriter();

        CsvSchema schema = mapper.schemaFor(Person.class)
                .withHeader();

        try (var seqWriter = mapper.writerWithSchemaFor(Person.class).with(schema).writeValues(writer)) {
            seqWriter.write(_single());
            seqWriter.writeAll(_multiple());
        }
        System.out.println(writer.toString());
    }

    @Test
    public void deserializePets() {
        String serializedPets = "[{'Mr. Bubbles','dog'}, {'Lilly','cat'}]";

        List<Pet> pets = PersonDeserializer.deserialize(serializedPets);

        assertThat(pets).hasSize(2);
    }

    @Test
    public void readPersonFromCsv() throws IOException {
        String serializedPersons = """
        name,pets
        Ali,"[{'Cutesy','cat'}, {'Tootsie','cat'}]"
        Jan,"[{'Mr. Bubbles','dog'}, {'Lilly','cat'}]"
        Tom,"[{'Fester','dog'}, {'Wednesday','cat'}]"
        Zoe,"[{'Jaeger','dog'}, {'Deira','cat'}]"
        """;

        CsvMapper mapper = new CsvMapper();
        SimpleModule module = new SimpleModule();

        module.addSerializer(Person.class, new PersonSerializer());
        module.addDeserializer(Person.class, new PersonDeserializer());

        mapper.registerModule(module);

        CsvSchema schema = mapper.schemaFor(Person.class).withHeader();
        ObjectReader reader = mapper.readerFor(Person.class).with(schema);

        MappingIterator<Person> i = reader.readValues(serializedPersons);

        while (i.hasNext()) {
            System.out.println("Person: " + i.next());
        }
    }

    @Getter
    @Builder
    @ToString
    //This will take precedence over mapper registered deserializers
    @JsonDeserialize(builder=Person.PersonBuilder.class)
    static class Person {
        private final String name;
        private final List<Pet> pets;

    }

    @Getter
    @Builder
    @ToString
    static class Pet {
        private final String name;
        private final String type;

    }

    static class PersonSerializer extends StdSerializer<Person> {
        private static final long serialVersionUID = 1L;

        public PersonSerializer() {
            this(Person.class);
        }

        public PersonSerializer(Class<Person> type) {
            super(type);
        }

        @Override
        public void serialize(Person value, JsonGenerator jgen, SerializerProvider provider)
          throws IOException, JsonProcessingException {
            jgen.writeStartObject();
            jgen.writeStringField("name", value.getName());
            jgen.writeStringField("pets", _toString(value.getPets()));
            jgen.writeEndObject();
        }

        private String _toString(List<Pet> pets) {
            return Arrays.toString(pets.stream()
                    .map(PersonSerializer::serialize)
                    .toArray(String[]::new));
        }

        private static String serialize(Pet pet) {
            return String.format("{'%s','%s'}", pet.name, pet.type);
        }

    }

    static class PersonDeserializer extends StdDeserializer<Person> {
        private static final long serialVersionUID = 1L;

        public PersonDeserializer() {
            this(Person.class);
        }

        public PersonDeserializer(Class<Person> type) {
            super(type);
        }

        @Override
        public Person deserialize(JsonParser p, DeserializationContext ctxt)
          throws IOException, JsonProcessingException {
            JsonNode node = p.getCodec().readTree(p);

            String name = node.get("name").asText();
            String petsSerialized = node.get("pets").asText();

            List<Pet> pets = deserialize(petsSerialized);

            return Person.builder().name(name).pets(pets).build();
        }

        private static List<Pet> deserialize(String serializedPets) {
            return Arrays.stream(serializedPets.replaceAll("^.\\{?|}?.$", "").split("},\s*\\{"))
                    .map(inner -> Arrays.stream(inner.replaceAll("^.'?|'?.$", "").split("','")).toList())
                    .map(l -> deserialize(l))
                    .collect(Collectors.toList());
        }

        private static Pet deserialize(List<String> l) {
            return Pet.builder().name(l.get(0)).type(l.get(1)).build();
        }
    }

    private static Person _single() {
        return Person.builder().name("Jan").pets(List.of(Pet.builder().name("Mr. Bubbles").type("dog").build(), Pet.builder().name("Lilly").type("cat").build())).build();
    }

    private static Person[] _multiple() {
        return List.of(_single(), _single(), _single()).toArray(Person[]::new);
    }

}
JanStureNielsen commented 1 year ago

Whoops -- wrong repo