FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.53k stars 1.38k forks source link

JsonParser#getCodec() is null in a custom JsonDeserializer #4461

Closed Feniksovich closed 7 months ago

Feniksovich commented 7 months ago

Search before asking

Describe the bug

Similar resolved issue: https://github.com/FasterXML/jackson-databind/issues/2038 I keep receive NPE on JsonParser#getCodec() in a custom JsonDeserializer when trying to deserialize my House POJO:

@Test
public void testHouseSerializationTest() throws JsonProcessingException {
   final ObjectMapper mapper = JsonMapper.builder()
            .addModules(
                    new PersonSerializationModule(),
                    new FlatSerializationModule(),
                    new HouseSerializationModule()
            ).build();

    final House house = new House("111-222-333", "A",
            new Person("A", "B", "C", "01.01.1970"),
            new Flat(1, 50,
                    new Person("A", "B", "C", "01.01.1970"),
                    new Person("D", "E", "F", "01.01.1970")
            ),
            new Flat(2, 50,
                    new Person("A", "B", "C", "01.01.1970"),
                    new Person("D", "E", "F", "01.01.1970")
            )
    );
    assertEquals(house, mapper.readValue(mapper.writeValueAsString(house), House.class));
}

There are also two registered JsonSerializer/JsonDeserializer for the Person and Flat POJOs and they work fine. But on House POJO deserialization I receive NPE on JsonParser#getCodec() invoke in the PersonDeserializer (see below).

Stacktrace ```java java.lang.NullPointerException: Cannot invoke "com.fasterxml.jackson.core.ObjectCodec.readTree(com.fasterxml.jackson.core.JsonParser)" because the return value of "com.fasterxml.jackson.core.JsonParser.getCodec()" is null at com.feniksovich.lab7.serializers.jackson.PersonSerializationModule$PersonDeserializer.deserialize(PersonSerializationModule.java:32) at com.feniksovich.lab7.serializers.jackson.PersonSerializationModule$PersonDeserializer.deserialize(PersonSerializationModule.java:29) at com.fasterxml.jackson.databind.DeserializationContext.readValue(DeserializationContext.java:992) at com.fasterxml.jackson.databind.DeserializationContext.readValue(DeserializationContext.java:979) at com.feniksovich.lab7.serializers.jackson.HouseSerializationModule$HouseDeserializer.deserialize(HouseSerializationModule.java:44) at com.feniksovich.lab7.serializers.jackson.HouseSerializationModule$HouseDeserializer.deserialize(HouseSerializationModule.java:38) at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4905) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3848) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3816) ```

Version Information

2.17.0

Reproduction

  1. Register serializers/deserializes.
  2. Create something House object and try to deserialize it.
  3. Receive NPE on JsonParser#getCodec().
PersonSerializationModule ```java public class PersonSerializationModule extends SimpleModule { public PersonSerializationModule() { addSerializer(Person.class, new PersonSerializer()); addDeserializer(Person.class, new PersonDeserializer()); } private static class PersonSerializer extends JsonSerializer { @Override public void serialize(Person person, JsonGenerator generator, SerializerProvider provider) throws IOException { generator.writeStartObject(); generator.writeStringField("fullName", person.getLastName() + " " + person.getFirstName() + " " + person.getPatronymic()); generator.writeStringField("birthDate", person.getBirthDate()); generator.writeEndObject(); } } private static class PersonDeserializer extends JsonDeserializer { @Override public Person deserialize(JsonParser parser, DeserializationContext context) throws IOException { final JsonNode tree = parser.getCodec().readTree(parser); final String[] fullName = tree.get("fullName").asText().split(" "); final String birthDate = tree.get("birthDate").asText(); return new Person(fullName[0], fullName[1], fullName[2], birthDate); } } } ```
FlatSerializationModule ```java public class FlatSerializationModule extends SimpleModule { public FlatSerializationModule() { addSerializer(Flat.class, new FlatSerializer()); addDeserializer(Flat.class, new FlatDeserializer()); } private static class FlatSerializer extends JsonSerializer { @Override public void serialize(Flat flat, JsonGenerator generator, SerializerProvider provider) throws IOException { generator.writeStartObject(); generator.writeNumberField("number", flat.getNumber()); generator.writeNumberField("area", flat.getArea()); generator.writeArrayFieldStart("owners"); for (Person person : flat.getOwners()) generator.writePOJO(person); generator.writeEndArray(); generator.writeEndObject(); } } private static class FlatDeserializer extends JsonDeserializer { @Override public Flat deserialize(JsonParser parser, DeserializationContext context) throws IOException { final JsonNode tree = parser.getCodec().readTree(parser); final int number = tree.get("number").asInt(); final float area = (float) tree.get("area").asDouble(); final List owners = new ArrayList<>(); for (JsonNode node : tree.get("owners")) { owners.add(context.readTreeAsValue(node, Person.class)); } return new Flat(number, area, owners.toArray(Person[]::new)); } } } ```
HouseSerializationModule ```java public class HouseSerializationModule extends SimpleModule { public HouseSerializationModule() { addSerializer(House.class, new HouseSerializer()); addDeserializer(House.class, new HouseDeserializer()); } private static class HouseSerializer extends JsonSerializer { @Override public void serialize(House house, JsonGenerator generator, SerializerProvider provider) throws IOException { generator.writeStartObject(); generator.writeStringField("cadastralId", house.getCadastralId()); generator.writeStringField("address", house.getAddress()); generator.writePOJOField("houseElder", house.getHouseElder()); generator.writeArrayFieldStart("flats"); for (Flat flat : house.getFlats()) generator.writePOJO(flat); generator.writeEndArray(); generator.writeEndObject(); } } private static class HouseDeserializer extends JsonDeserializer { @Override public House deserialize(JsonParser parser, DeserializationContext context) throws IOException { final JsonNode tree = parser.getCodec().readTree(parser); final String cadastralId = tree.get("cadastralId").asText(); final String address = tree.get("address").asText(); final Person houseElder = context.readValue(tree.get("houseElder").traverse(), Person.class); final Flat[] flats = context.readValue(tree.get("flats").traverse(), Flat[].class); return new House(cadastralId, address, houseElder, flats); } } } ```

Expected behavior

Successful deserialization of the House object.

Additional context

No response

Feniksovich commented 7 months ago

It seems I just should use DeserializationContext to access other registered (de-)serializatiors, isn't it?

JooHyukKim commented 7 months ago

At first I assumed resolution of ObjectCodec is not made at deserialize(JsonParser jp, DeserializationContext ctxt) stage, but others' deserialization works.

Could you please share how House, Person and Flat classes are declared?

JooHyukKim commented 7 months ago

Also, could you try deserializing like below? Below is from jackson-databind test suite. I can't make time yet, to confirm but seems pretty reasonable way to go .

        @Override
        public Leaf deserialize(JsonParser jp, DeserializationContext ctxt)
                throws IOException
        {
            JsonNode tree = (JsonNode) jp.readValueAsTree();
            Leaf leaf = new Leaf();
            leaf.value = tree.get("value").intValue();
            return leaf;
        }
Feniksovich commented 7 months ago

At first I assumed resolution of ObjectCodec is not made at deserialize(JsonParser jp, DeserializationContext ctxt) stage, but others' deserialization works.

Correct and it's strange thing. I'm new to Jackson, I've used some examples from the internet to implement custom deserializers. Examples include line like parser.getCodec().readTree(parser) to obtain JSON tree without setting the ObjectCodec explicitly.

Could you please share how House, Person and Flat classes are declared?

Please see this gist: https://gist.github.com/Feniksovich/4711bf5570661c915cd9936f56e0b932

Also, could you try deserializing like below? [ ... ]

I replaced JsonNode tree = parser.getCodec().readTree(parser) with JsonNode tree = parser.readValueAsTree() in every deserializator and got following exception:

Stacktrace ```java java.lang.IllegalStateException: No ObjectCodec defined for parser, needed for deserialization at com.fasterxml.jackson.core.JsonParser._codec(JsonParser.java:2547) at com.fasterxml.jackson.core.JsonParser.readValueAsTree(JsonParser.java:2541) at com.feniksovich.lab7.serializers.jackson.PersonSerializationModule$PersonDeserializer.deserialize(PersonSerializationModule.java:33) at com.feniksovich.lab7.serializers.jackson.PersonSerializationModule$PersonDeserializer.deserialize(PersonSerializationModule.java:29) at com.fasterxml.jackson.databind.DeserializationContext.readValue(DeserializationContext.java:992) at com.fasterxml.jackson.databind.DeserializationContext.readValue(DeserializationContext.java:979) at com.feniksovich.lab7.serializers.jackson.HouseSerializationModule$HouseDeserializer.deserialize(HouseSerializationModule.java:47) at com.feniksovich.lab7.serializers.jackson.HouseSerializationModule$HouseDeserializer.deserialize(HouseSerializationModule.java:40) at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4905) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3848) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3816) at com.feniksovich.lab7.jackson.JacksonSerializationModulesTest.testHouseSerializationTest(JacksonSerializationModulesTest.java:53) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ```

However, I just tried to use DeserializationContext to read tree and it's works. Is it correct and expected solution?

@Override
public Person deserialize(JsonParser parser, DeserializationContext context) throws IOException {
    final JsonNode tree = context.readTree(parser);
    // ...
}        
JooHyukKim commented 7 months ago

However, I just tried to use DeserializationContext to read tree and it's works. Is it correct and expected solution?

Sure, I checked the jackson-databind test suite and there seems to be a couple of usecases doing the same. if it works, why not. Sorry again for late reply! 🙏🏼 @Feniksovich

Feniksovich commented 7 months ago

However, I just tried to use DeserializationContext to read tree and it's works. Is it correct and expected solution?

Sure, I checked the jackson-databind test suite and there seems to be a couple of usecases doing the same. if it works, why not. Sorry again for late reply! 🙏🏼 @Feniksovich

Thank you for your assistance, @JooHyukKim!

JooHyukKim commented 7 months ago

Just wondering how it should be implemented by design, but anyway thank you for assistance! @JooHyukKim

I am hoping that we will have more guidance on how to implement custom modules and de/serializers, eventually. Thank you for the feedback!

cowtowncoder commented 7 months ago

Quick note: ideally, you would never need to call JsonParser.getCodec() from a deserializer, custom or otherwise. If you do, it's a flaw somewhere. Instead, everything needed should be accessible via either JsonParser or -- in most cases -- DeserializationContext (like suggested). So, f.ex instead of

        @Override
        public Leaf deserialize(JsonParser jp, DeserializationContext ctxt)
                throws IOException
        {
            JsonNode tree = (JsonNode) jp.readValueAsTree();

it should be possible to use

            JsonNode tree = ctxt.readTree(jp);

Linkage of JsonParser.getCodec() is bit problematic, although it should also work. The trouble (aside from it not being assigned for some reason) is that when JsonParser calls methods in ObjectMapper, the effective DeserializationContext is not available and new one gets created. This in turn can have negative consequences on access to things.

So, yes, if at all possible, first look into DeserializationContext for functionality.

Feniksovich commented 7 months ago

So, yes, if at all possible, first look into DeserializationContext for functionality.

@cowtowncoder, thank you for detailed explanation! I really appreciate prompt feedback from your team.

cowtowncoder commented 7 months ago

Thank you @Feniksovich !