JSON mapping for polymorphic POJOs with univariate and multivariate properties

I am currently trying to create a REST client that communicates with a specific online service. This online service is returning some JSON responses that I want to map to Java objects using Jackson .

An example JSON response would be:

{
    "id" : 1,
    "fields" : [ {
      "type" : "anniversary",
      "value" : {
        "day" : 1,
        "month" : 1,
        "year" : 1970
      }
    }, {
      "type" : "birthday",
      "value" : {
        "day" : 1,
        "month" : 1,
        "year" : 1970
      }
    }, {
      "type" : "simple",
      "value" : "simple string"
    },{
       "type": "name",
       "value": {
           "firstName": "Joe",
           "lastName": "Brown"
       }
    } ]
}

      

NOTE:

  • this structure contains a simple id and a set of Field instances, each with a type and value
  • the structure of the values โ€‹โ€‹is determined by the external type of the property
  • in the example given there are 3 types -> date, name and string with one value
  • birthday and anniversary types match the date structure
  • there are several types that display a single string of values โ€‹โ€‹such as email type, twitterId type, company type, etc.

My problem is that I can't seem to map this structure to Java objects correctly

Here's what I've done so far. Following are the classes and their Jackson annotations (getters and setters omitted).

public class Contact {
    private int id;
    private List<Field> fields;
}

public class Field {
    private FieldType type;
    private FieldValue value;
}

public enum FieldType {
    EMAIL, NICKNAME, NAME, ADDRESS, BIRTHDAY, ANNIVERSARY
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type",
        defaultImpl = SingleFieldValue.class)
@JsonSubTypes({
        @JsonSubTypes.Type(value = NameFieldValue.class, name = "name"),
        @JsonSubTypes.Type(value = DateFieldValue.class, name = "anniversary"),
        @JsonSubTypes.Type(value = DateFieldValue.class, name = "birthday"),
        @JsonSubTypes.Type(value = SingleFieldValue.class, name = "nickname"),
        @JsonSubTypes.Type(value = SingleFieldValue.class, name = "email"),
        //other types that map to SingleFieldValue
})
public abstract FieldValue {
}


public class NameFieldValue extends FieldValue {

    private String firstName;
    private String lastName;
 }

public class DateFieldValue extends FieldValue {

    private int day;
    private int month;
    private int year;
}

public class SingleFieldValue extends FieldValue {

    private String value;
}

      

ObjectMapper does not contain any configuration, the default configuration is used.

What suggestions do you have to display them correctly? I would like to avoid creating custom deserializers and just traverse Json objects like JsonNode.

NOTE . I apologize in advance for the lack of information to make this issue clear enough. Please point out any problems with my wording.

+3


source to share


1 answer


You have used an abstract class at the FieldValue level so you can use it in the FIeld class. In this case, you can construct an object with type = email and value = address, which can lead to some problems ...

I would recommend creating specific classes for each type with a specific FieldValue type. The following code serializes / deserializes JSON from / to the required format from / to POJO:

public class Main {
    String json = "{\"id\":1,\"fields\":[{\"type\":\"SIMPLE\",\"value\":\"Simple Value\"},{\"type\":\"NAME\",\"value\":{\"firstName\":\"first name\",\"lastName\":\"last name\"}}]}";

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

        String json = objectMapper.writeValueAsString(generate());
        System.out.println(json);

        System.out.println(objectMapper.readValue(json, Contact.class));
    }

    private static Contact generate() {
        SimpleField simpleField = SimpleField.builder().type(FieldType.SIMPLE).value("Simple Value").build();

        NameFieldValue nameFieldValue = NameFieldValue.builder().firstName("first name").lastName("last name").build();
        NameField nameField = NameField.builder().type(FieldType.NAME).value(nameFieldValue).build();

        return Contact.builder().id(1).fields(Arrays.asList(simpleField, nameField)).build();
    }
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = SimpleField.class, name = "SIMPLE"),
        @JsonSubTypes.Type(value = NameField.class, name = "NAME")
})
interface Field {
    FieldType getType();
    Object getValue();
}

enum FieldType {
    SIMPLE, NAME
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class Contact {
    private int id;
    private List<Field> fields;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class SimpleField implements Field {
    private FieldType type;
    private String value;

    @Override
    public FieldType getType() {
        return this.type;
    }

    @Override
    public String getValue() {
        return this.value;
    }
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class NameField implements Field {
    private FieldType type;
    private NameFieldValue value;

    @Override
    public FieldType getType() {
        return this.type;
    }

    @Override
    public Object getValue() {
        return this.value;
    }
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class NameFieldValue {
    private String firstName;
    private String lastName;
}

      



I've used the lombok library here to keep code to a minimum and avoid creating getters / setters as well as constructors. You can remove lombok annotations and add getters / setters / constructors and the code will work the same.

So the idea is that you have a Contact class (which is the root of your JSON) with a list of fields (where the field is the interface). Each field type has its own implementation, for example NameField implements a field and has a NameFieldValue property as a property. The trick here is that you can change the declaration of the getValue () method and declare that it returns a generic interface or object (I used an object, but the interface will work too).

This solution does not require any custom serializers / deserializers and is easy to maintain.

+2


source







All Articles