Jackson adding additional fields to JSON throws com.fasterxml.jackson.databind.exc.InvalidDefinitionException

2.6k Views Asked by At

I am using Jackson to serialize my Java POJO classes. In addition to fields, I have in Java POJO, I would like to add some additional information in JSON I am writing my own custom CustomClassSerializer. If I use this class and register to ObjectMapper then I get the error:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Type id handling not implemented for type org.acme.Tiger (by serializer of type org.acme.CustomModule$CustomClassSerializer)

I am unable to understand what might be going wrong here. If I remove the custom registered model then everything works perfectly.

Can someone please let me know what may be the cause of this issue? I am currently using Jackson 2.13.2 latest version dependencies: jackson-core, jackson-databind, jackson-annotations, jackson-datatype-jdk8:

Following is the sample code:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.*;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "type")
@JsonSubTypes({@JsonSubTypes.Type(value = Bat.class, name = "Bat"),
        @JsonSubTypes.Type(value = Tiger.class, name = "Tiger")})
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Animal {
    private String type;
    private String name;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonTypeName("Tiger")
public class Tiger extends Animal {
    private String livingType;
    private String foundIn;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonTypeName("Bat")
public class Bat extends Animal{
    private String livingType;
    private String foundIn;
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;

import java.io.IOException;

public class CustomModule extends SimpleModule {
    public CustomModule() {
        addSerializer(Tiger.class, new CustomClassSerializer());
    }

    private static class CustomClassSerializer extends JsonSerializer {
        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
            jgen.writeStartObject();
            jgen.writeObjectField("my_extra_field1", "some data");
            jgen.writeObjectField("my_extra_field2", "some more data");
            JavaType javaType = provider.constructType(Tiger.class);
            BeanDescription beanDesc = provider.getConfig().introspect(javaType);
            JsonSerializer<Object> serializer = BeanSerializerFactory.instance.createSerializer(provider, javaType);
            serializer.unwrappingSerializer(null).serialize(value, jgen, provider);
            jgen.writeEndObject();
        }
    }
}
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TestMain {
    public static void main(String[] args) throws JsonProcessingException {
        final ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.registerModule(new CustomModule());

        Tiger tiger = new Tiger();
        tiger.setType("Tiger");
        tiger.setName("Shera");
        tiger.setFoundIn("Ground");
        tiger.setLivingType("Tree");
        System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(tiger));
    }
}

I would like to know what are the causes for the above-mentioned exception.

1

There are 1 best solutions below

2
On

I don't think I can write really efficient Jackson code while being a Gson-oriented guy, but I guess you could incorporate the following Q/As to resolve your question:

Also, I'd like to refactor your code (a lot) in order to make it flexible by using the strategy design pattern (where necessary), and demonstrate how it works using with simple unit tests.

public final class InterceptingSerializerModifier<T>
        extends BeanSerializerModifier {

    public interface IAppender<T> {

        void append(JsonGenerator generator, T value)
                throws IOException;

    }

    private final Class<T> baseClass;
    private final IAppender<? super T> appender;

    private InterceptingSerializerModifier(final Class<T> baseClass, final IAppender<? super T> appender) {
        this.baseClass = baseClass;
        this.appender = appender;
    }

    public static <T> BeanSerializerModifier create(final Class<T> baseClass, final IAppender<? super T> appender) {
        return new InterceptingSerializerModifier<>(baseClass, appender);
    }

    @Override
    public JsonSerializer<?> modifySerializer(final SerializationConfig config, final BeanDescription description, final JsonSerializer<?> serializer) {
        if ( !baseClass.isAssignableFrom(description.getBeanClass()) ) {
            return serializer;
        }
        @SuppressWarnings("unchecked")
        final JsonSerializer<? super T> castSerializer = (JsonSerializer<? super T>) serializer;
        return InterceptingJsonSerializer.create(castSerializer, appender);
    }

    private static final class InterceptingJsonSerializer<T>
            extends JsonSerializer<T> {

        private static final NameTransformer identityNameTransformer = new NameTransformer() {
            @Override
            public String transform(final String name) {
                return name;
            }

            @Override
            public String reverse(final String transformed) {
                return transformed;
            }
        };

        private final JsonSerializer<? super T> unwrappedSerializer;
        private final IAppender<? super T> appender;

        private InterceptingJsonSerializer(final JsonSerializer<? super T> unwrappedSerializer, final IAppender<? super T> appender) {
            this.unwrappedSerializer = unwrappedSerializer;
            this.appender = appender;
        }

        private static <T> JsonSerializer<T> create(final JsonSerializer<? super T> serializer, final IAppender<? super T> appender) {
            return new InterceptingJsonSerializer<>(serializer.unwrappingSerializer(identityNameTransformer), appender);
        }

        @Override
        public void serializeWithType(final T value, final JsonGenerator generator, final SerializerProvider provider, final TypeSerializer serializer)
                throws IOException {
            serializer.writeTypePrefix(generator, serializer.typeId(value, JsonToken.START_OBJECT));
            doSerialize(value, generator, provider);
            serializer.writeTypeSuffix(generator, serializer.typeId(value, JsonToken.START_OBJECT));
        }

        @Override
        public void serialize(final T value, final JsonGenerator generator, final SerializerProvider provider)
                throws IOException {
            generator.writeStartObject();
            doSerialize(value, generator, provider);
            generator.writeEndObject();
        }

        private void doSerialize(final T value, final JsonGenerator generator, final SerializerProvider provider)
                throws IOException {
            unwrappedSerializer.serialize(value, generator, provider);
            appender.append(generator, value);
        }

    }

}
public final class InterceptingSerializerModifierTest {

    private static final BeanSerializerModifier unit = InterceptingSerializerModifier.create(
            IValue.class,
            (generator, value) -> {
                if ( value instanceof Foo foo ) {
                    generator.writeObjectField("@extra.message", "this is from foo: " + foo.value);
                } else if ( value instanceof Bar bar ) {
                    generator.writeObjectField("@extra.message", "this is from bar: " + bar.value);
                } else {
                    generator.writeObjectField("@extra.message", "something else...");
                }
            }
    );

    private static final ObjectMapper objectMapper = new ObjectMapper()
            .registerModule(new SimpleModule()
                    .setSerializerModifier(unit)
            );

    private static Stream<Arguments> test() {
        return Stream.of(
                Arguments.of(
                        "{\"@type\":\"FOO\",\"value\":1,\"@extra.message\":\"this is from foo: 1\"}",
                        new Foo(1)
                ),
                Arguments.of(
                        "{\"@type\":\"BAR\",\"value\":2.0,\"@extra.message\":\"this is from bar: 2.0\"}",
                        new Bar(2)
                )
        );
    }

    @ParameterizedTest
    @MethodSource
    public void test(final String expectedJson, final IValue actualValue)
            throws JsonProcessingException {
        Assertions.assertEquals(expectedJson, objectMapper.writeValueAsString(actualValue));
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
    @JsonSubTypes({
            @JsonSubTypes.Type(value = Foo.class, name = "FOO"),
            @JsonSubTypes.Type(value = Bar.class, name = "BAR")
    })
    private sealed interface IValue
            permits Foo, Bar {
    }

    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    private static final class Foo
            implements IValue {

        @Getter
        private final int value;

    }

    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    private static final class Bar
            implements IValue {

        @Getter
        private final double value;

    }

}

I hope the code above is pretty self-explaining. You may also want to enhance the code in order to make identityNameTransformer injectable too, if you need to use extra property name transformations.


Unrelated to the subject, but duplicating JSON object property names like my_extra_field in your question may be considered not recommended (not sure if vulnerable). See more: Does JSON syntax allow duplicate keys in an object? .