Jackson polymorphism issue without enableDefaultTyping - java

I'm facing a weird issue during deserializing of a JSON file that has polymorphic types.
My json file looks like this
{
"name": "abc",
"version": 1,
"fileNames": [
"xyz.abc",
"xyz/abc"
]
"vehicles": [
"java.util.ArrayList",
[
{
"name": "Car"
},
{
"name": "Train"
},
]
]
}
My code looks like:
private static final ObjectMapper OBJECT_MAPPER;
static {
OBJECT_MAPPER = new ObjectMapper();
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OBJECT_MAPPER.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
OBJECT_MAPPER.addMixIn(Vehicle.class, VehicleMixIn.class);
}
#JsonCreator
public Config(
#JsonProperty(value = "name", required = true) final String name,
#JsonProperty(value = "version") final Integer version,
#JsonProperty(value = "Vehicles") List<Vehicle> listofVehicles,
#JsonProperty(value = "paths") final List<String> paths, {
fileName = name;
versionNumber = version;
vehicles = listofVehicles;
mPaths = paths;
}
/**
* A JSONMixin helper class to deserialize Vehicles.
*/
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "name")
#JsonSubTypes({
#JsonSubTypes.Type(name = "Car", value = Car.class),
#JsonSubTypes.Type(name = "Train", value = Train.class),
#JsonSubTypes.Type(name = "Bus", value = Bus.class)
})
private abstract class VehicleMixIn {
public VehicleMixIn() {
}
}
The way I read the json file
try (BufferedReader reader = Files.newBufferedReader(configFile.toPath(), StandardCharsets.UTF_8)) {
config = Config.fromJson(reader);
}
I'm unable to read the polymorphic vehicle type.
I get the following error:
com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (VALUE_STRING), expected FIELD_NAME: missing property 'name' that is to contain type id (for class abc.def.Vehicle)
This works if I do OBJECT_MAPPER.enableDefaultTyping(), however then I'm unable to read the 'paths' field. How do I make this mixIn work?
I've tried all types of defaultTyping

Related

How to identify different types automatically using Jackson mapper

I have a simple situation. I have a main DTO class with the following fields:
AnimalDTO
public class AnimalDTO {
#JsonCreator
public AnimalDTODTO(#JsonProperty("error") boolean error,
#JsonProperty("errorMessage") String errorMessage,
#JsonProperty("cat") CatDTO cat,
#JsonProperty("dog") DogDTO dog) {
this.error = error;
this.errorMessage = errorMessage;
this.cat = cat;
this.dog = dog;
}
private boolean error;
private String errorMessage;
private CatDTO cat;
private DogDTO dog;
}
CatDTO:
public class CatDTO {
#JsonCreator
public CatDTO(#JsonProperty("catNumber") String catNumber,
#JsonProperty("catState") String catState) {
this.catNumber = catNumber;
this.catState = catState;
}
private String catNumber;
private String catState;
}
DogDTO:
public class DogDTO {
#JsonCreator
public DogDTO(#JsonProperty("dogNumber") String dogNumber,
#JsonProperty("dogState") String dogState
#JsonProperty("dogColor") String dogColor
#JsonProperty("dogBalance") BigDecimal dogBalance) {
this.dogNumber = dogNumber;
this.dogState = dogState;
this.dogColor = dogColor;
this.dogBalance = dogBalance;
}
private String dogNumber;
private String dogState;
private String dogColor;
private BigDecimal dogBalance;
}
and from external API I have responses (and I can't change it) for dog like:
{
"dogNumber": "23",
"dogState": "good",
"dogColor": "pink",
"dogBalance": 5
}
and for Cat:
{
"catNumber": "1",
"catState": "good"
}
And I want to use Jackson mapper like this: objectMapper.readValue(stringJson, AnimalDTO.class);
I was thinking to add in AnimalDTO:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#Type(value = CatDTO.class, name = "cat"),
#Type(value = DogDTO.class, name = "dog")
})
but it's not working.
How to handle my case in best way?
Your code will work just fine (without any #JsonTypeInfo or #JsonSubTypes annotations) if the JSON that you get from the external API is as follows:
{
"dog": {
"dogNumber": "23",
"dogState": "good",
"dogColor": "pink",
"dogBalance": 5
},
"cat": {
"catNumber": "1",
"catState": "good"
}
}
If this does not look similar to the JSON you receive then you need to add it to your answer so that we can help you further.

Jackson JSON deserialization using JsonParser is skipping the first key value pair due to condition check

I am using Jackson to deserialize a JSON. The input JSON can be of 2 types CustomerDocument or Single Customer. CustomerDocument will have a CustomerList which can consist of a huge number of Customer and the Single Customer will have just a Single Customer. Hence, the Jackson has to handle 2 things:
Identify if the provided JSON is a CustomerDocument, if so then deserialize the elements in CustomerList one by one rather than storing the whole thing into List so as to reduce the memory usage.
Identify if the provided JSON is a single Customer and if so then deserialize the customer directly.
I am able to achieve this and everything is working as expected but when I provide the CustomerDocument then it's unable to read the #Context key-value pair as it has been already read during the check for single Customer (as mentioned by you in point 2). I guess the below code would make the problems clear:
Following is the JSON I am trying to deserialize:
{
"#context": [
"https://stackoverflow.com",
{
"example": "https://example.com"
}
],
"isA": "CustomerDocument",
"customerList": [
{
"isA": "Customer",
"name": "Batman",
"age": "2008"
}
]
}
Following is my Customer POJO class:
#Data
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Customer implements BaseResponse {
private String isA;
private String name;
private String age;
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
#JsonSubTypes({
#JsonSubTypes.Type(value = Customer.class, name = "Customer")})
interface BaseResponse {
}
Following is my Main class which will read the JSON InputStream and make the decision whether the provided input JSON is CustomerList or Single Customer and then deserialize accordingly:
public class JacksonMain {
public static void main(String[] args) throws IOException {
final InputStream jsonStream = JacksonMain.class.getClassLoader().getResourceAsStream("Customer.json");
final JsonParser jsonParser = new JsonFactory().createParser(jsonStream);
final ObjectMapper objectMapper = new ObjectMapper();
jsonParser.setCodec(objectMapper);
//Goto the start of the document
jsonParser.nextToken();
try {
BaseResponse baseResponse = objectMapper.readValue(jsonParser, BaseResponse.class);
System.out.println("SINGLE EVENT INPUT" + baseResponse.toString());
} catch (Exception e) {
System.out.println("LIST OF CUSTOMER INPUT");
//Go until the customerList has been reached
while (!jsonParser.getText().equals("customerList")) {
System.out.println("Current Token Name : " + jsonParser.getCurrentName());
if (jsonParser.getCurrentName() != null && jsonParser.getCurrentName().equalsIgnoreCase("#context")) {
System.out.println("WITHIN CONTEXT");
}
jsonParser.nextToken();
}
jsonParser.nextToken();
//Loop through each object within the customerList and deserilize them
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
final JsonNode customerNode = jsonParser.readValueAsTree();
final String eventType = customerNode.get("isA").asText();
Object event = objectMapper.treeToValue(customerNode, BaseResponse.class);
System.out.println(event.toString());
}
}
}
}
Following is the output I am getting:
LIST OF CUSTOMER INPUT
Current Token Name : isA
Customer(isA=Customer, name=Batman, age=2008)
As we can see it's printing only Current Token Name: isA I would expect it to print isA and #Context because it's present before the isA. But I am aware that it's not printing because it has already passed that due to the following line of code in try block:
BaseResponse baseResponse = objectMapper.readValue(jsonParser, BaseResponse.class);
Following is a Single Customer JSON (just for the reference and this is working fine):
{
"#context": [
"https://stackoverflow.com",
{
"example": "https://example.com"
}
],
"isA": "Customer",
"name": "Batman",
"age": "2008"
}
Can someone please suggest to me how can I achieve this and is there a better workaround for this issue?
Please note:
The CustomerList can have a lot of Customers hence I do not want to store the whole CustomerList into some List as it can take a lot of memories. Hence, I am using JsonParser so I can read one JsonToken at a time.
Also, I do not want to create a CustomerList class rather than that I want to read one Customer at a time and deserialize it.
The JSON structure cannot be modified as it's coming from another application and it's a standard format for my application.
I have fixed the code, I removed the catch block. CUSTOMERS_LIST property differentiate between single and multiple customers.
JacksonMain.java
public class JacksonMain {
private static final String CUSTOMERS_LIST = "customerList";
public static void main(String[] args) throws IOException {
final InputStream jsonStream = JacksonMain.class.getClassLoader().getResourceAsStream("Customers.json");
final JsonParser jsonParser = new JsonFactory().createParser(jsonStream);
final ObjectMapper objectMapper = new ObjectMapper();
jsonParser.setCodec(objectMapper);
TreeNode treeNode = objectMapper.readTree(jsonParser);
if (Objects.isNull(treeNode.get(CUSTOMERS_LIST))) {
BaseResponse baseResponse = objectMapper.treeToValue(treeNode, BaseResponse.class);
System.out.println("SINGLE EVENT INPUT: " + baseResponse);
} else {
System.out.println("LIST OF CUSTOMER INPUT");
ArrayNode customerList = (ArrayNode) treeNode.get(CUSTOMERS_LIST);
for (JsonNode node : customerList) {
BaseResponse event = objectMapper.treeToValue(node, BaseResponse.class);
System.out.println(event.toString());
}
}
}
}
I have created two resources json files
Customer.json
{
"#context": [
"https://stackoverflow.com",
{
"example": "https://example.com"
}
],
"isA": "Customer",
"name": "Batman",
"age": "2008"
}
Customers.json
{
"#context": [
"https://stackoverflow.com",
{
"example": "https://example.com"
}
],
"isA": "CustomerDocument",
"customerList": [
{
"isA": "Customer",
"name": "Batman",
"age": "2008"
}
]
}
CustomerDocument.java
#Data
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
#JsonInclude(JsonInclude.Include.NON_NULL)
#ToString
public class CustomerDocument implements BaseResponse {
#JsonProperty
private String isA;
#JsonProperty(value="name")
private String name;
#JsonProperty(value="age")
private String age;
}
Customer.java
#Data
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Customer implements BaseResponse {
private String isA;
private String name;
private String age;
}
BaseResponse.java
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
#JsonSubTypes({
#JsonSubTypes.Type(value = Customer.class, name = "Customer"),
#JsonSubTypes.Type(value = CustomerDocument.class, name = "CustomerDocument")})
#JsonIgnoreProperties(ignoreUnknown = true)
interface BaseResponse {
}
In interface I have added JsonIgnoreProperties annotation and I have added one more missed JsonSubType. [CustomerDocument]
Both cases are working now.
Performance tests:
Size: 1,065,065 records / customers handled
Time in seconds: 30.208
Finally, I was able to get it working, Instead of reading the #context in catch block I tried reading it before the try-catch.
public class JacksonMain {
public static void main(String[] args) throws IOException {
final InputStream jsonStream = JacksonMain.class.getClassLoader().getResourceAsStream("Customer.json");
final JsonParser jsonParser = new JsonFactory().createParser(jsonStream);
final ObjectMapper objectMapper = new ObjectMapper();
jsonParser.setCodec(objectMapper);
//Goto the start of the document
jsonParser.nextToken();
//Go until the customerList has been reached
while (!jsonParser.getText().equals("isA")) {
System.out.println("Current Token Name : " + jsonParser.getCurrentName());
if (jsonParser.getCurrentName() != null && jsonParser.getCurrentName().equalsIgnoreCase("#context")) {
System.out.println("WITHIN CONTEXT");
}
jsonParser.nextToken();
}
try {
BaseResponse baseResponse = objectMapper.readValue(jsonParser, BaseResponse.class);
System.out.println("SINGLE EVENT INPUT" + baseResponse.toString());
} catch (Exception e) {
System.out.println("LIST OF CUSTOMER INPUT");
//Loop through each object within the customerList and deserilize them
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
final JsonNode customerNode = jsonParser.readValueAsTree();
final String eventType = customerNode.get("isA").asText();
Object event = objectMapper.treeToValue(customerNode, BaseResponse.class);
System.out.println(event.toString());
}
}
}
}

Deserializing a List of Strings with defaultTyping

I'm facing a weird issue during deserializing of a JSON file that has two lists declared in different ways.
My json file looks like this
{
"name": "abc",
"version": 1,
"fileNames": [
"xyz.abc",
"xyz/abc"
]
"vehicles": [
"java.util.ArrayList",
[
{
"name": "Car"
},
{
"name": "Train"
},
]
]
}
My code looks like:
private static final ObjectMapper OBJECT_MAPPER;
static {
OBJECT_MAPPER = new ObjectMapper();
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OBJECT_MAPPER.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
OBJECT_MAPPER.addMixIn(Vehicle.class, VehicleMixIn.class);
OBJECT_MAPPER.enableDefaultTyping();
}
#JsonCreator
public Config(
#JsonProperty(value = "name", required = true) final String name,
#JsonProperty(value = "version") final Integer version,
#JsonProperty(value = "Vehicles") List<Vehicle> listofVehicles,
#JsonProperty(value = "paths") final List<String> paths, {
fileName = name;
versionNumber = version;
vehicles = listofVehicles;
mPaths = paths;
}
/**
* A JSONMixin helper class to deserialize Vehicles.
*/
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "name")
#JsonSubTypes({
#JsonSubTypes.Type(name = "Car", value = Car.class),
#JsonSubTypes.Type(name = "Train", value = Train.class),
#JsonSubTypes.Type(name = "Bus", value = Bus.class)
})
private abstract class VehicleMixIn {
public VehicleMixIn() {
}
}
The way I read the json file
try (BufferedReader reader = Files.newBufferedReader(configFile.toPath(), StandardCharsets.UTF_8)) {
config = Config.fromJson(reader);
}
I'm unable to read the polymorphic vehicle type.
I get the following error:
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'xyz.abc' into a subtype of [collection type; class java.util.List, contains [simple type, class java.lang.String]
This works if I remove OBJECT_MAPPER.enableDefaultTyping(), however then I'm unable to read the 'Vehicles' field. How do I make this work?
I've tried all types of defaultTyping and also tried to use a custom deserializer.

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"
}

Polymorphic deserialization jackson

I am trying to implement polymorphic deserialization in jackson and trying to make the same model work in two places.
I have ShopData object
public class ShopData extends Embeddable implements Serializable
{
private final int id;
private final String name;
private final String logoImageUrl;
private final String heroImageUrl;
public ShopData(#JsonProperty(value = "id", required = true) int id,
#JsonProperty(value = "name", required = true) String name,
#JsonProperty(value = "logoImageUrl", required = true) String logoImageUrl,
#JsonProperty(value = "heroImageUrl", required = true) String heroImageUrl)
{
this.id = id;
this.name = name;
this.logoImageUrl = logoImageUrl;
this.heroImageUrl = heroImageUrl;
}
}
My Embeddable object looks like this
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.WRAPPER_OBJECT)
#JsonSubTypes({#JsonSubTypes.Type(value = AnotherObject.class, name = "AnotherObject"),
#JsonSubTypes.Type(value = ShopData.class, name = "shop")
})
public abstract class Embeddable
{
}
I am trying to make this model work in two places. This model works as expected.
public Order(#JsonProperty(value = "_embedded", required = true) Embeddable embedded)
{
this.embedded = (ShopData) embedded;
}
"_embedded": {
"shop": {
"id": 1,
"name": "",
"freshItems": 5,
"logoImageUrl": "",
"heroImageUrl": "",
"_links": {
"self": {
"href": "/shops/1"
}
}
}
While this doesn't
public ShopList(#JsonProperty(value = "entries", required = true) List<ShopData> entries)
{
this.entries = Collections.unmodifiableList(entries);
}
{
"entries": [
{
"id": 1,
"name": "",
"freshItems": 5,
"logoImageUrl": "",
"heroImageUrl": "",
"_links": {
"self": {
"href": "/shops/1"
}
}
}
]
}
And throws error :Could not resolve type id 'id' into a subtype
I understand the error but do not know how to resolve this. I would like to be able to use the same model in both cases. Is this possible?
Just found out the answer myself. Should have simply added this anotation
#JsonTypeInfo(use= JsonTypeInfo.Id.NONE)
public class ShopData extends Embeddable implements Serializable

Categories

Resources