FasterXML / jackson-modules-java8

Set of support modules for Java 8 datatypes (Optionals, date/time) and features (parameter names)
Apache License 2.0
398 stars 116 forks source link

`Optional` deserialization, serialization ignore `contentConverter` #294

Closed richardsonwk closed 7 months ago

richardsonwk commented 9 months ago

I was surprised to find that contentConverter does not work with Optional, but contentUsing does. I think this is effectively the same as https://github.com/FasterXML/jackson-modules-java8/issues/97.

    /**
     * Pretend like this class cannot be annotated.
     */
    public static final class StringWrapper {
        private final String mWrapped;

        public StringWrapper(final String s) {
            mWrapped = s;
        }

        @Override
        public boolean equals(final Object other) {
            if (other instanceof StringWrapper) {
                return mWrapped.equals(((StringWrapper) other).mWrapped);
            }

            return false;
        }

        public String get() {
            return mWrapped;
        }
    }

    /**
     * A class in my library.
     */
    public static final class SomeClassToSerialize {
        @JsonDeserialize(contentConverter = StringWrapperIn.class)
        @JsonSerialize(contentConverter = StringWrapperOut.class)
        @JsonProperty("field")
        private final Optional<StringWrapper> mField;

        /**
         * For Jackson reading only.
         */
        private SomeClassToSerialize() {
            mField = null;
        }

        public SomeClassToSerialize(final Optional<StringWrapper> value) {
            mField = value;
        }

        @Override
        public boolean equals(final Object other) {
            if (other instanceof SomeClassToSerialize) {
                return mField.equals(((SomeClassToSerialize) other).mField);
            }

            return false;
        }
    }

    public static final class StringWrapperIn extends StdConverter<String, StringWrapper> {
        @Override
        public StringWrapper convert(final String value) {
            System.out.println("Working on the way in");
            return new StringWrapper(value);
        }
    }

    public static final class StringWrapperOut extends StdConverter<StringWrapper, String> {
        @Override
        public String convert(final StringWrapper value) {
            System.out.println("Working on the way out");
            return value.get();
        }
    }

    public static void main(final String[] args) throws JsonProcessingException {
        final SomeClassToSerialize original =
            new SomeClassToSerialize(Optional.of(new StringWrapper("value")));

        final ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new Jdk8Module());

        final String out =
            mapper.writer()
                .with(INDENT_OUTPUT)
                .writeValueAsString(original);

        System.out.println(out);

        final SomeClassToSerialize in =
            mapper.readValue(out, SomeClassToSerialize.class);

        assert original.equals(in);
    }

The output is

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class StringWrapper and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
...

A slight tweak to instead specify a content serializer fixes it:

    public static final class SomeClassToSerialize {
        @JsonDeserialize(contentUsing = StringWrapperDeserializer.class)
        @JsonSerialize(contentUsing = StringWrapperSerializer.class)
        @JsonProperty("field")
        private final Optional<StringWrapper> mField;
        ...
    }

    public static final class StringWrapperDeserializer extends StdDelegatingDeserializer<StringWrapper> {
        public StringWrapperDeserializer() {
            super(new StringWrapperIn());
        }

        /*
         * Somewhat unrelated, I don't follow why this override is necessary, since it does exactly what the superclass
         * does - except for throwing for any subclass.
         */
        @Override
        protected StdDelegatingDeserializer<StringWrapper> withDelegate(
            final Converter<Object, StringWrapper> converter,
            final JavaType delegateType,
            final JsonDeserializer<?> delegateDeserializer) {

            return new StdDelegatingDeserializer<>(converter, delegateType, delegateDeserializer);
        }
    }

    public static final class StringWrapperSerializer extends StdDelegatingSerializer {
        public StringWrapperSerializer() {
            super(new StringWrapperOut());
        }
    }

The output is then

Working on the way out
{
  "field" : "value"
}
Working on the way in

This is not a difficult workaround, so it's no problem - just unexpected.

cowtowncoder commented 8 months ago

Thank you for reporting this @richardsonwk -- it definitely sounds like bug and is not intentional.