Search code examples
javaspringspring-bootjacksonjackson-databind

Jackson deserialize array of mixed types using @JsonTypeInfo with extra fields


I have a json file which contains an array of mixed types. Its structure look like this:

{
  "operations": [
    {
      "extension": {
        "serviceId": "id",
        "serviceType": "type"
      },
      "name": "name",
      "tags": "tags"
    },
    {
      "core": {
        "config": {
          "a": 90,
          "b": 45
        },
        "displayName": "displayName"
      },
      "name": "name",
      "tags": "tags"
    },
    {
      "extension": {
        "serviceId": "abc",
        "serviceType": "xyz"
      },
      "name": "name",
      "tags": "tags"
    }
  ]
}

Follow this topic Stackoverflow I create my model for json mapping of mixed types.

My java model:

@Data
public class Payload {

  private List<OperationElement> operations;

  @Data
  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
  @JsonSubTypes({ @JsonSubTypes.Type(value = Extension.class, name = "extension"),
    @JsonSubTypes.Type(value = Core.class, name = "core") })
  public static abstract class OperationElement {
    private String name;
    private String tags;
  }

  @Data
  @JsonRootName("extension")
  public static class Extension extends OperationElement {
    private String serviceType;
    private String serviceId;
  }

  @Data
  @JsonRootName("core")
  public static class Core extends OperationElement {

    private Config config;
    private String displayName;

    @Data
    public static class Config {

      private Long a;
      private Long b;

    }
  }

  public static void main(String[] args) throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    Payload operationsPayload = mapper.readValue(
      Files.readAllBytes(Paths.get("C:\\desc", "file.json")), Payload.class);
    System.out.println(operationsPayload );
  }
}

I got this error when trying to parse the json to java model:

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (FIELD_NAME), expected END_OBJECT: expected closing END_OBJECT after type information and deserialized value
  line: 8, column: 7] (through reference chain: com.pojo.Payload["operations"]->java.util.ArrayList[0])
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
    at com.fasterxml.jackson.databind.DeserializationContext.wrongTokenException(DeserializationContext.java:1799)
    at com.fasterxml.jackson.databind.DeserializationContext.reportWrongTokenException(DeserializationContext.java:1533)
    at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserializer._deserialize(AsWrapperTypeDeserializer.java:124)
    at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserializer.deserializeTypedFromObject(AsWrapperTypeDeserializer.java:52)
    at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:349)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3609)

If I remove

"name": "name",
"tags": "tags"

The code can run.

But I must have both these key-value in the json structure.

Any solution please suggest me. Thanks in advance.


Solution

  • The JsonTypeInfo.As#WRAPPER_OBJECT works only for the serialization process while you are trying to deserialize your json input. In this case you can use the JsonTypeInfo.Id#DEDUCTION to deduce the correct type from the existing properties. Taking for example a simplified version of your json input like below:

    {
      "operations": [
        {
          "extension": {
            "serviceId": "id",
            "serviceType": "type"
          },
          "name": "name",
          "tags": "tags"
        }
      ]
    }
    

    You have an array containing an anonymous object with the three properties extension, name, tag that can be deserialized like below:

    @Data
    public class Payload {
        List<OperationWrapper> operations;
    }
    
    @Data
    @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
    @JsonSubTypes({
        @JsonSubTypes.Type(value = ExtensionWrapper.class)
    })
    public abstract class OperationWrapper {}
    
    @Data
    public class ExtensionWrapper extends OperationWrapper {
        private Extension extension;
        private String name;
        private String tags;
    }
    
    @Data
    public class Extension {
        private String serviceType;
        private String serviceId;
    }
    
    Payload payload = mapper.readValue(json, Payload.class);
    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(payload));
    

    Output:

    {
      "operations" : [ {
        "extension" : {
          "serviceType" : "type",
          "serviceId" : "id"
        },
        "name" : "name",
        "tags" : "tags"
      } ]
    }
    

    The same mechanism can be applied to other classes you want to be deserialized.