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.
source to share
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.
source to share