Mapping JSON to polymorphic POJOs with single- and multi-properties - java

I am currently trying to develop a REST client that communicates with a certain online service. This online service returns some JSON responses which I wish to map to Java objects using Jackson.
An example of a 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 the following:
this structure contains a simple id, and a collection of Field instances, each having a type and a value
the value structure is determined by the external property type
in the example given, there are 3 types -> date, name, and single value string
the birthday and anniversary types both match the date structure
there are several types which map to a single value string, like email type, twitterId type, company type etc.
My problem is that I do not seem to be able to correctly map this structure to the Java objects
Here's what I've done so far. The following are the classes and their Jackson annotations(the getters and setters are 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;
}
The ObjectMapper does not contain any configuration, the default configuration is used.
What suggestions do you have to correctly map these? I would like to avoid making custom deserializers and just traverse Json objects, like JsonNode.
NOTE: I apologize in advance for any lack of information to make this issue clear enough. Please state any problems with my formulation.

You have used an abstract class on the FieldValue level to use it in FIeld class. In that case you can construct the object with type=email and value=address which can lead to some issues...
I would recommend to create a specific classes for every type with specific FieldValue type.
The following code is serializing/deserializing JSON from/to 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 have used lombok library here just to minimize the code and avoiding creating getters/setters as well as constructors. You can delete lombok annotations and add getters/setters/constructors and code will work the same.
So, the idea is that you have a Contact class (which is root of your JSON) with a list of Fields (where Field is an interface). Every Field type has own implementation like NameField implements Field and has NameFieldValue as a property. The trick here is that you can change getValue() method declaration and declare that it returns the common interface or Object (I used Object but interface will work as well).
This solution doesn't require any custom serializers/deserializers and easy in maintenance.

Related

Unable to remove tag when serializing object to yaml using jackson-dataformat-yaml

I use jackson (jackson-databinder & jackson-dataformat-yaml) to serialize polymorphism classes to both json and yaml. And I use one class property as type resolver. I can remove the class metadata info in json, but in yaml it still contain the class meta info in tag. How can I remove that. Here's my sample code:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type")
#JsonSubTypes({
#Type(value = Car.class, name = "car"),
#Type(value = Truck.class, name = "truck") })
public interface Vehicle {
String getName();
}
#Value
public static class Car implements Vehicle {
String name;
String type = "car";
#JsonCreator
public Car(#JsonProperty("name") final String name) {
this.name = requireNonNull(name);
}
}
#Value
public static class Truck implements Vehicle {
String name;
String type = "truck";
#JsonCreator
public Truck(#JsonProperty("name") final String name) {
this.name = requireNonNull(name);
}
}
#Value
public static class Vehicles {
List<Vehicle> vehicles;
#JsonCreator
public Vehicles(#JsonProperty("vehicles") final List<Vehicle> vehicles) {
super();
this.vehicles = requireNonNull(vehicles);
}
}
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper MAPPER = new ObjectMapper();
ObjectMapper YAML_MAPPER = YAMLMapper.builder()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.build();
final Vehicles vehicles = new Vehicles(ImmutableList.of(new Car("Dodge"), new Truck("Scania")));
final String json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(vehicles);
System.out.println(json);
final String yaml = YAML_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(vehicles);
System.out.println(yaml);
}
And here's json and yaml output:
{
"vehicles" : [ {
"name" : "Dodge",
"type" : "car"
}, {
"name" : "Scania",
"type" : "truck"
} ]
}
vehicles:
- !<car>
name: "Dodge"
type: "car"
- !<truck>
name: "Scania"
type: "truck"
There's no class meta info in json output. But in yaml, there're still the tag which contains the class meta info. Is it possible to remove that in yaml as json? Thanks
You can disable the YAMLGenerator.Feature#USE_NATIVE_OBJECT_ID yaml feature that is enabled by default for indicate types in the serialization. So in the construction of your ObjectMapper YAML_MAPPER mapper you can disable this feature like below obtaining the expected result:
ObjectMapper YAML_MAPPER = YAMLMapper.builder()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID)
.build();

Deserializing json subtype based on parent property

I have a json coming with a dynamic attribute child, like below:
{
"label":"Some label",
"attribute": { <--- Dynamic attribute object
"type": "TEXT", <--- Field used to map `attribute` dynamic (inside child object)
"languages":[
{
"language":"en_EN",
"text":"do you approve?"
}
]
}
}
Or
{
"label":"Some label",
"attribute": {
"type": "NUMBER",
"value: "10.0"
}
}
I am able to deserialize above json properly using #JsonSubTypes with this code:
#Data
public class Field {
private String label;
private Attribute attribute;
}
#Data
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = Attribute.NumberAttribute.class, name = "NUMBER"),
#JsonSubTypes.Type(value = Attribute.TextAttribute.class, name = "TEXT")
})
public class Attribute {
private AttributeType type;
#Data
public static class TextAttribute extends Attribute {
List<Language> languages;
}
#Data
public static class NumberAttribute extends Attribute {
String value;
}
#Data
public static class Language {
private String text;
private String language;
}
}
However, the problem I have is that I have to use type field coming inside the attribute object, and I would need to move the type to the parent object. The ending json should be like this:
{
"type": "TEXT", <--- Field used to map `attribute` dynamic (in parent object)
"label":"Some label",
"attribute": { <--- Dynamic attribute object
"languages":[
{
"language":"en_EN",
"text":"do you approve?"
}
]
}
}
Or
{
"type": "NUMBER",
"label":"Some label",
"attribute": {
"value: "10.0"
}
}
I couldn't find any way to use a parent field (or json path way) to use the type property being outside the dynamic subtype. Do you know how I can do this?
You can achieve that by adding include = As.EXTERNAL_PROPERTY to #JsonTypeInfo. You just have to move the annotation to the field.
See the JavaDoc for EXTERNAL_PROPERTY:
Inclusion mechanism similar to PROPERTY, except that property is included one-level higher in hierarchy [...]
Here's an example:
#Data
class Field {
private String label;
private AttributeType attributeType;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.EXTERNAL_PROPERTY, property = "attributeType")
private Attribute attribute;
}
#Data
#JsonSubTypes({
#JsonSubTypes.Type(value = Attribute.NumberAttribute.class, name = "NUMBER"),
#JsonSubTypes.Type(value = Attribute.TextAttribute.class, name = "TEXT")
})
abstract class Attribute {
#Data
public static class TextAttribute extends Attribute {
List<Language> languages;
}
#Data
public static class NumberAttribute extends Attribute {
String value;
}
#Data
public static class Language {
private String text;
private String language;
}
}
enum AttributeType {
NUMBER, TEXT;
}
I'm posting this as an alternative in case accepted answer doesn't work for others:
#Data
public class Field {
private String label;
private AttributeType type;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = As.EXTERNAL_PROPERTY)
#JsonSubTypes({
#JsonSubTypes.Type(value = Attribute.NumberAttribute.class, name = "NUMBER"),
#JsonSubTypes.Type(value = Attribute.TextAttribute.class, name = "TEXT")
})
private Attribute attribute;
}
#Data
public class Attribute {
#Data
public static class TextAttribute extends Attribute {
List<Language> languages;
}
#Data
public static class NumberAttribute extends Attribute {
String value;
}
#Data
public static class Language {
private String text;
private String language;
}
}

How to ignore case for POJO if it extends an abstract class in Spring Jackson?

I know we can ignore case for input JSON by adding property in application.yml as:
spring:
jackson:
mapper:
accept_case_insensitive_properties: true
But if my POJO extends an abstract class, it is not working and my JSON is not being parsed.
My abstract class:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "event")
#JsonSubTypes({
#JsonSubTypes.Type(value = Orders.class, name = "orders"),
#JsonSubTypes.Type(value = WorkOrders.class, name = "workOrders")
})
public abstract class ElasticDocument {
// Fields and getter/setter
}
My Pojo:
#JsonIgnoreProperties(ignoreUnknown = true)
#Data
public class Orders extends ElasticDocument {
//other fields
private List<OrderLine> orderLines;
}
Input JSON which I am getting from input has different case e.g.
{
"event": "orders",
"OrderNo": 12345,
"Status": "Created",
"CustomerZipCode": "23456",
"CustomerFirstName": "firstname1",
"orderType": "PHONEORDER",
"customerLastName": "lastname1",
"OrderLines": [
{
"LineName": "sample"
}
]
}
My contoller method where I am using this ElasticDocument object:
#PostMapping("save")
public Orders save(#RequestBody ElasticDocument elasticDocument) {
return elasticsearchRepository.save((Orders) elasticDocument);
}
I am using Spring-boot version 2.2.4
I think you forgot to add #type to your request JSON.#type is to identify the type of ElasticDocument being serialized.
Here is a example that i tried in my local system with minimum fields in class:
ElasticDocument.java
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
#JsonSubTypes({
#JsonSubTypes.Type(value = Orders.class, name = "Orders"),
#JsonSubTypes.Type(value = WorkOrders.class, name = "workOrders")
})
public abstract class ElasticDocument {
private Integer docId;
private String docName;
// getters and setters
}
Orders.java
public class Orders extends ElasticDocument{
private Integer orderId;
private String orderName;
// getters and setters
}
WorkOrders.java
public class WorkOrders extends ElasticDocument{
private Integer workOrderId;
private String workOrderName;
// getters and setters
}
StackOverflowController.java
#RestController
#RequestMapping("/api/v1")
public class StackOverflowController {
#PostMapping("/orders")
ElasticDocument createOrder(#RequestBody ElasticDocument order){
return order;
}
}
When i send data like this to my endpoint (Please note the attributes name in json are lowercase)
{
"#type":"workOrders",
"docId":123,
"docName":"XXXX",
"orderid":45,
"ordername":"shoe",
"workorderid":324,
"workordername":"dsf"
}
It is converted to workOrders response:
{
"#type": "workOrders",
"docId": 123,
"docName": "XXXX",
"workOrderId": 324,
"workOrderName": "dsf"
}
And when i changed the #type to Orders in request then i will get Order response:
{
"#type": "Orders",
"docId": 123,
"docName": "XXXX",
"orderId": 45,
"orderName": "shoe"
}

How do I parse a JSON array into different objects using Jackson on Android?

I'm trying to parse JSON like the following into object using Jackson on Android (Note: I'm not in control of the JSON format - the format comes from Yammer)
"references": [
{
"type": "user",
"id": 12345678,
"name": "Wex"
},
{
"type": "message",
"id": 12345679,
"body":
{
"plain":"A short message"
}
},
{
"type": "thread",
"id": 12345670,
"thread_starter_id": 428181699
}
]
The problem is that each entry in references is a different type of object with different properties. As a start I've got:
public static class Reference
{
public String type;
public String id;
}
I'd rather avoid putting all potential properties in one object like:
public static class Reference
{
public static class Body
{
public String plain;
}
public String type;
public String id;
public String name;
public Body body;
public String thread_starter_id;
}
And want to use separate classes that are created dependant on the type value like:
public static class ReferenceUser extends Reference
{
public String name;
}
public static class ReferenceMessage extends Reference
{
public static class Body
{
public String plain;
}
public Body body;
}
public static class ReferenceThread extends Reference
{
public String thread_starter_id;
}
So... what's the best way of getting Jackson to parse the JSON like that?
I'm currently parsing it quite simply like this:
ObjectMapper mapper = new ObjectMapper();
Reference[] references = mapper.readValue(json, Reference[].class);
you can do something like this with Jackson:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(name = "user", value = ReferenceUser.class),
#JsonSubTypes.Type(name = "message", value = ReferenceMessage.class),
#JsonSubTypes.Type(name = "thread", value = ReferenceThread.class)
})
public class Reference {
int id;
String name;
}
This way you will generate subclasses.
John

How can I deserialize a JSON array of "name" "value" pairs to a Pojo using Jackson

I want to deserialize the following JSON object:
{
"id":"001",
"module_name":"Users",
"name_value_list":
{
"user_name": {"name":"user_name", "value":"admin"},
"full_name": {"name":"full_name", "value":"LluĂ­s Pi"},
"city": {"name":"full_name", "value":"Barcelona"},
"postal_code": {"name":"postal_code", "value":"08017"},
...
}
}
into some Java object like this:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
public class UserEntry
{
private String id;
private String moduleName;
private Person nameValueList;
public String getId()
{
return id;
}
public String getModuleName()
{
return moduleName;
}
public Person getPerson()
{
return nameValueList;
}
}
where Person is the following class:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
class Person
{
private String userName;
private String fullName;
private String city;
private String postalCode;
}
using Jackson but I get a deserialization error.
If I change the type of field nameValueList to a Map all the deserialization process goes with no problem and I get a map where the key is the "name" value and the value is the "value" value.
So my question is: is there any simple, or no so simple, way to deserialize this kind of JSON object to a Java Pojo with properties prop_1, prop_2, prop_3and prop_4?
{
"name_value_list":
{
"prop_1": {"name":"prop_1", "value":"value_1"},
"prop_2": {"name":"prop_2", "value":"value_2"},
"prop_3": {"name":"prop_3", "value":"value_3"},
"prop_4": {"name":"prop_4", "value":"value_4"},
...
}
}
Not very simple and not very clean. However you can do it by implementing a any setter field for the JSON attributes in the Person class which don't match any attribute on your UserEntry POJO.
#JsonAnySetter
public void putUserField(String userKey, Map<String, String> userValue)
throws NoSuchFieldException {
String actualFieldName = getActualFieldName(userKey);
Field field = this.getClass().getDeclaredField(actualFieldName);
field.setAccessible(true);
ReflectionUtils.setField(field, this, userValue.get("value"));
}
private String getActualFieldName(String userKey) {
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, userKey);
}
In addition to that, I had to change the Jackson attributes for the Person class to
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
getterVisibility = JsonAutoDetect.Visibility.NONE)
for it to work for attributes like "city" which don't need any name transformation because jackson tries to directly set the field which fails.

Categories

Resources