Immutables: Can't deserialize nested empty object with Jackson

Created on 4 May 2016  Â·  3Comments  Â·  Source: immutables/immutables

Having trouble deserializing a value in the example below:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import org.immutables.value.Value;

import java.io.IOException;

public class Main {
    @Value.Immutable
    @JsonSerialize(as = ImmutableOuterObject.class)
    @JsonDeserialize(as = ImmutableOuterObject.class)
    public static abstract class OuterObject {
        public abstract EmptyObject emptyObject();
    }

    @Value.Immutable
    @JsonSerialize(as = ImmutableEmptyObject.class)
    @JsonDeserialize(as = ImmutableEmptyObject.class)
    public static abstract class EmptyObject {
    }

    public static void main(String[] args) throws IOException {
        final ObjectMapper objectMapper = new ObjectMapper();

        objectMapper.enableDefaultTyping();
        objectMapper.registerModule(new GuavaModule());

        final String serialized = objectMapper.writeValueAsString(
            ImmutableOuterObject
                .builder()
                .emptyObject(
                    ImmutableEmptyObject
                        .builder()
                        .build()
                )
                .build()
        );

        System.out.println(serialized);

        final OuterObject deserialized = objectMapper.readValue(serialized, ImmutableOuterObject.class);
    }
}

If I (de)serialize e.g. an instance of ImmutableEmptyObject directly it works, but if it's one level deep, the following exception is thrown:

➜  immutable mvn exec:java -Dexec.mainClass="Main"
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building immutables 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.4.0:java (default-cli) @ immutables ---
{"emptyObject":["ImmutableEmptyObject",{}]}
[WARNING]
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293)
    at java.lang.Thread.run(Thread.java:745)
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (END_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.util.Map
 at [Source: {"emptyObject":["ImmutableEmptyObject",{}]}; line: 1, column: 41] (through reference chain: Json["emptyObject"])
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:216)
    at com.fasterxml.jackson.databind.DeserializationContext.wrongTokenException(DeserializationContext.java:962)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._locateTypeId(AsArrayTypeDeserializer.java:127)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:93)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:58)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserializeWithType(MapDeserializer.java:375)
    at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:42)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1119)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:298)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:133)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:58)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1017)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:488)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:95)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:260)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:163)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:135)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1119)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:298)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:133)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3789)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2779)
    at Main.main(Main.java:42)
    ... 6 more
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.758s
[INFO] Finished at: Wed May 04 11:00:30 PDT 2016
[INFO] Final Memory: 14M/309M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.4.0:java (default-cli) on project immutables: An exception occured while executing the Java class. null: InvocationTargetException: Unexpected token (END_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.util.Map
[ERROR] at [Source: {"emptyObject":["ImmutableEmptyObject",{}]}; line: 1, column: 41] (through reference chain: Json["emptyObject"])
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

This seems to be due to the fact that the @JsonCreator that is generated on ImmutableEmptyObject is different in the case of an object with no fields vs. an object that has at least one. The creator generated in the zero-field case:

  /**
   * @param json A JSON-bindable data structure
   * @return An immutable value type
   * @deprecated Do not use this method directly, it exists only for the <em>Jackson</em>-binding infrastructure
   */
  @Deprecated
  @JsonCreator
  static ImmutableEmptyObject fromJson(Map<String, Object> json) {
    ImmutableEmptyObject.Builder builder = ImmutableEmptyObject.builder();
    return builder.build();
  }

and in the one-field case:

  /**
   * Utility type used to correctly read immutable object from JSON representation.
   * @deprecated Do not use this type directly, it exists only for the <em>Jackson</em>-binding infrastructure
   */
  @Deprecated
  @JsonDeserialize
  static final class Json extends Main.EmptyObject {
    Integer foo;
    @JsonProperty
    public void setFoo(Integer foo) {
      this.foo = foo;
    }
    @Override
    public Integer foo() { throw new UnsupportedOperationException(); }
  }

  /**
   * @param json A JSON-bindable data structure
   * @return An immutable value type
   * @deprecated Do not use this method directly, it exists only for the <em>Jackson</em>-binding infrastructure
   */
  @Deprecated
  @JsonCreator
  static ImmutableEmptyObject fromJson(Json json) {
    ImmutableEmptyObject.Builder builder = ImmutableEmptyObject.builder();
    if (json.foo != null) {
      builder.foo(json.foo);
    }
    return builder.build();
  }

If I adapt the code generated in the one-field case, make the Json class empty, and put it into the generated empty-field source file, the code works as expected.

Is there something I'm missing here or is there an issue with the code being generated?

bug

Most helpful comment

It works! Thanks a ton for quick response & fix.

All 3 comments

Thank you for reporting this! I've tried it and all boils down to having .enableDefaultTyping() and @JsonCreator with Map<String, Object> parameter which are non compatible. I would say that it's a Jackson issue, but we can mitigate this if remove Map<String, Object> optimization and use empty Json object for empty value objects, which in theory should solve this.

Please check freshly released v2.1.19 for the fix

It works! Thanks a ton for quick response & fix.

Was this page helpful?
0 / 5 - 0 ratings