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
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.
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());
}
}
}
}
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.
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"
}
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