Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
1 task done
Feniksovich opened this issue Mar 30, 2024 · 10 comments
Closed
1 task done

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

Feniksovich opened this issue Mar 30, 2024 · 10 comments
Labels
to-evaluate Issue that has been received but not yet evaluated

Comments

@Feniksovich
Copy link

Feniksovich commented Mar 30, 2024

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

Similar resolved issue: #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.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
public class PersonSerializationModule extends SimpleModule {

    public PersonSerializationModule() {
        addSerializer(Person.class, new PersonSerializer());
        addDeserializer(Person.class, new PersonDeserializer());
    }

    private static class PersonSerializer extends JsonSerializer<Person> {
        @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<Person> {
        @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
public class FlatSerializationModule extends SimpleModule {

    public FlatSerializationModule() {
        addSerializer(Flat.class, new FlatSerializer());
        addDeserializer(Flat.class, new FlatDeserializer());
    }

    private static class FlatSerializer extends JsonSerializer<Flat> {
        @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<Flat> {
        @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<Person> 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
public class HouseSerializationModule extends SimpleModule {

    public HouseSerializationModule() {
        addSerializer(House.class, new HouseSerializer());
        addDeserializer(House.class, new HouseDeserializer());
    }

    private static class HouseSerializer extends JsonSerializer<House> {
        @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<House> {
        @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 Feniksovich added the to-evaluate Issue that has been received but not yet evaluated label Mar 30, 2024
@Feniksovich
Copy link
Author

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

@JooHyukKim
Copy link
Member

JooHyukKim commented Mar 31, 2024

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
Copy link
Member

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
Copy link
Author

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.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);
    // ...
}        

@Feniksovich Feniksovich changed the title com.fasterxml.jackson.core.JsonParser.getCodec() is null in a custom JsonDeserializer JsonParser#getCodec() is null in a custom JsonDeserializer Mar 31, 2024
@JooHyukKim
Copy link
Member

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
Copy link
Author

Feniksovich commented Apr 1, 2024

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
Copy link
Member

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
Copy link
Member

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
Copy link
Author

Feniksovich commented Apr 1, 2024

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
Copy link
Member

Thank you @Feniksovich !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
to-evaluate Issue that has been received but not yet evaluated
Projects
None yet
Development

No branches or pull requests

3 participants