I need to decode the following JSON-Structure:
{
"id":1
"categories": [
value_of_category1,
value_of_category2,
value_of_category3
]
}
The object I am trying to deserialize into is of the following class:
class MyClass {
public Integer id;
public Category1 category1;
public Category2 category2;
public Category3 category3;
...
}
public enum Category1{
...
}
public enum Category2{
...
}
public enum Category3{
...
}
In the JSON the first entry of the categories-Array is always a value of the enum Category1, the second entry is always a value of the enum Category2 and the third entry is always a value of the enum Category3.
How can I deserialize this JSON into my class structure using Jackson?
You can create your custom deserializer by extending the JsonDeserializer<T> class.
Here is a sample implementation to fit your needs
public class MyClassDeserializer extends JsonDeserializer<MyClass>{
#Override
public MyClass deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
int id = (Integer) ((IntNode) node.get("id")).numberValue();
JsonNode categories = node.get("categories");
Iterator<JsonNode> iter = categories.elements();
while(iter.hasNext()){
//create Category object based on an incrementing counter
}
MyClass myClass = //compose myClass from the values deserialized above.
return myClass;
}
}
To use it, you just have to annotate MyClass with #JsonDeserialize pointing to your custom deserializer.
#JsonDeserialize(using = MyClassDeserializer.class)
public class MyClass {
private Integer id;
private Category1 category1;
private Category2 category2;
private Category3 category3;
}
Not related, but as a good practice, make all your fields private then expose public setters and getters in order to control the fields of the class.
Related
I'm experimenting some troubles with Jackson deserialization in Java. I've made 2 solutions, and I can't resolve the problem. Problem? I got my result with the property duplicated, a field it's duplicated after jackson deserialization. (My problem is exact the same as this question: Avoid duplicate field generated by JsonTypeInfo in Jackson and no one could give you an answer at the time)
First at all, I have the following class:
#JsonIgnoreProperties(ignoreUnknown = true)
public class Instance {
#JsonProperty("id")
private String id;
#JsonProperty("name")
private String name;
#JsonProperty("type")
private InstanceType type;
}
What I'm triying to do, is just instantiate an object of type 'Instance', save it and read it. And with solution 2, the object is saved with the type duplicated (type appear as array that contain 'name', 'firs_type', for example or 'second_type) depends on what I create. With solution 1, I can save the object ok, but when I try to read it, I fall on a jackson exception casting.
Solution 1:
#JsonDeserialize(using = InstanceTypeDeserializer.class)
public interface InstanceType {
String value();
}
#JsonDeserialize(as = HardInstanceType.class)
public enum HardInstanceType implements InstanceType {
FIRST_TYPE("first_type"),
SECOND_TYPE("second_type")
private String value;
HardInstanceType(String value) {
this.value = value;
}
#JsonValue
public String value() {
return value;
}
}
#JsonDeserialize(as = SoftInstanceType.class)
public enum SoftInstanceType implements InstanceType {
//.. types implementaion similar as HardInstanceType
}
public class InstanceTypeDeserializer extends JsonDeserializer<InstanceType> {
#Override
public InstanceType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
if(root.get("name").asText().equals("hard")) {
return mapper.readValue(root.toString(), HardInstanceType.class);
} else {
return mapper.readValue(root.toString(), SoftInstanceType.class);
}
}
}
The problem with this solution, is that when I try to get the data stored, and map to the class, I get the following error:
exception parsing json:
com.fasterxml.jackson.databind.JsonMappingException: class
com.fasterxml.jackson.databind.node.TextNode cannot be cast to class
com.fasterxml.jackson.databind.node.ObjectNode
(com.fasterxml.jackson.databind.node.TextNode and
com.fasterxml.jackson.databind.node.ObjectNode are in unnamed module
of loader org.springframework.boot.loader.LaunchedURLClassLoader
#1a3e8e24) (through reference chain:
java.util.ArrayList[0]->com.project.package.xxxx.Instance["type"])
Solution 2
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "name")
#JsonSubTypes({
#JsonSubTypes.Type(value = HardInstanceType.class, name = "hard") })
public interface InstanceType {
String value();
}
The problem with this solution, is that when I save the data, when I create an Instance object, and store it, in the data Stored, I get the following:
"id": "1",
"name": "hard",
"type": [
"hard",
"first_type"
]
what is not correct, in type should be store just "first_type" (what is stored with solution 1, but I can't read it haha).
Of course, Instace class is more complex and with more fields, I reduce it here, just for the example.
I need help with this, thank you very much in advance.
Finally, I could solve the problem.
I post this just in case someone else need it.
Add a property to my HardInstanceType class.
public enum HardInstanceType implements InstanceType {
FIRST_TYPE("first_type"),
SECOND_TYPE("second_type");
private String value;
public String hardTypeIdentifierSer = "hardTypeIdentifierSer";
HardInstanceType(String value) {
this.value = value;
}
#JsonValue
public String value() {
return value;
}
}
Then, in the deserializer:
public class InstanceTypeDeserializer extends JsonDeserializer {
#Override
public InstanceType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
TreeNode node = jp.readValueAsTree();
if (node.get("hardTypeIdentifierSer") != null) {
return jp.getCodec().treeToValue(node, HardInstanceType.class);
}
}
I am trying to convert following JSON to Java object and ending up with UnrecognizedPropertyException.
{
"5214": [{
"name": "sdsds",
"age": "25",
"address": null
},
{
"name": "sdfds",
"age": "26",
"address": null
}]
}
Here "5214" is the random key that I get. I can covert it by modifying JSON little bit. But I want to know whether any possible way to convert the mentioned JSON. I even tried with following snippet taking some reference.
public class SampleTest {
private Map<String, List<EmployeeDetails>> employeeDetails = new HashMap<String, List<EmployeeDetails>>();
public Map<String, List<EmployeeDetails>> getEmployeeDetails() {
return employeeDetails;
}
public void setEmployeeDetails(Map<String, List<EmployeeDetails>> employeeDetails) {
this.employeeDetails = employeeDetails;
}
}
public class EmployeeDetails {
private String name;
private String age;
private String address;
//Getters and Setters
}
Can someone guide me on this?
Use Type Reference (Import Jackson Package for Java)
TypeReference<Map<String, List<EmployeeDetails>>> typeReference = new TypeReference<Map<String, List<EmployeeDetails>>>()
{
};
Map<String, List<EmployeeDetails>> employeeDetails = new ObjectMapper().readValue(jsonString, typeReference);
Check something from that
Maybe:
public class Data {
// String contain the Key, for example: 5214
Map<String, List<EmployeeDetails>> employeeDetails =
new HashMap<String,List<EmployeeDetails>>();
public Data() {
}
#JsonAnyGetter
public Map<String, List<EmployeeDetails>> getEmployeeDetails() {
return employeeDetails;
}
}
I would use custom deserializer with few helper classes. To make the code (matter of opinion I guess) clearer, create the list object:
#SuppressWarnings("serial")
#Getter #Setter
public class EmployeeDetailsList extends ArrayList<EmployeeDetails> {
// this will hold the arbitrary name of list. like 5214
private String name;
}
Then this list seems to be inside an object, say Wrapper:
#Getter
#RequiredArgsConstructor
#JsonDeserialize(using = WrapperDeserializer.class)
public class Wrapper {
private final EmployeeDetailsList employeeDetailsList;
}
So there is annotation #JsonDeserializer that handles deserializing Wrapper. It is not possible to directly deserialize unknown field names to some defined type so we need to use mechanism like this custom deserializer that inspects what is inside Wrapper and determines what to deserialize and how.
And here is how the deserializer works:
public class WrapperDeserializer extends JsonDeserializer<Wrapper> {
private final ObjectMapper om = new ObjectMapper();
#Override
public Wrapper deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
TreeNode node = p.readValueAsTree();
// This is the place for caution. You should somehow know what is the correct node
// Here I happily assume there is just the one and first
String fName = node.fieldNames().next();
EmployeeDetailsList edl = om.readValue(node.get(fName).toString(),
EmployeeDetailsList.class);
edl.setName(fName);
return new Wrapper(edl);
}
}
Please check it carefully it is not perfect in sense finding alwasy the correct node and maybe the instantiation can be done in other ways better. But it shoudl give you a hunch how it could be done.
I want to consume a json with jax-rs my method stamp look like that.
#PostMapping("/test")
public ResponseEntity<String> consumeJson(#RequestBody TestPojo testPojo)
My json look like that
{
"code": "<code>",
"display": "<display>",
"activities": [
{
"categoryCode": "drug",
"drugDisplay" : "Ceforanide"
},{
"categoryCode": "observation",
"measurementWeight" : "80kg",
}
]
}
And i have the following pojos
public class TestPojo implements Serializable{
private String code;
private String display;
private List<ActivityPojo> activities;
// Getters & Setters
}
Now i have a super class and couple of classes inherit from it
public class ActivityPojo implements Serializable{
private String categoryCode;
}
The child classes
public class DrugPojo extends ActivityPojo implements Serializable{
private String drugDisplay;
// Getters & Setters
}
public class ObservationPojo extends ActivityPojo implements Serializable{
private String measurementWeight;
// Getters & Setters
}
Inside my webservice method i want to do something like that
List<ActivityPojo> activities = testPojo.getActivities();
for(int i = 0; i < activities.size(); i++){
if( activities.get(i) instanceof DrugPojo){
// do stuff
}
else if( activities.get(i) instanceof ObservationPojo){
// do stuff
}
}
So can polymorphically serialize my json in order to do that. Any help would be appreciated.
This question is very interresting so I did a few tests.
If I understood correctly the problem, I think this class (and the inner one) can solve it :
#Component
#SuppressWarnings("serial")
public class ActivityPojoJsonModule extends SimpleModule {
public ActivityPojoJsonModule() {
this.addDeserializer(ActivityPojo.class, new ActivityPojoDeserializer());
}
public static class ActivityPojoDeserializer extends JsonDeserializer<ActivityPojo> {
#Override
public ActivityPojo deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
ObjectCodec codec = parser.getCodec();
JsonNode node = codec.readTree(parser);
if(this.isDrug(node)) {
return codec.treeToValue(node, DrugPojo.class);
}
return codec.treeToValue(node, ObservationPojo.class);
}
private boolean isDrug(JsonNode node) {
return node.get("categoryCode").asText().equals("drug");
}
}
}
It adds a component to the Spring context that will deserialize ActivityPojo with a logic based on the value of the field categoryCode. You just have to add this class in the a scanned package and it will override the default behaviour of Jackson.
Consider the following json, getting from an public API:
anyObject : {
attributes: [
{
"name":"anyName",
"value":"anyValue"
},
{
"name":"anyName",
"value":
{
"key":"anyKey",
"label":"anyLabel"
}
}
]
}
As you can see, sometimes the value is a simple string and sometimes its an object. Is it somehow possible to deserialize those kind of json-results, to something like:
class AnyObject {
List<Attribute> attributes;
}
class Attribute {
private String key;
private String label;
}
How would I design my model to cover both cases. Is that possible ?
Despite being hard to manage as others have pointed out, you can do what you want. Add a custom deserializer to handle this situation. I rewrote your beans because I felt your Attribute class was a bit misleading. The AttributeEntry class in the object that is an entry in that "attributes" list. The ValueObject is the class that represents that "key"/"label" object. Those beans are below, but here's the custom deserializer. The idea is to check the type in the JSON, and instantiate the appropriate AttributeEntry based on its "value" type.
public class AttributeDeserializer extends JsonDeserializer<AttributeEntry> {
#Override
public AttributeEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
String name = root.get("name").asText();
if (root.get("value").isObject()) {
// use your object mapper here, this is just an example
ValueObject attribute = new ObjectMapper().readValue(root.get("value").asText(), ValueObject.class);
return new AttributeEntry(name, attribute);
} else if (root.get("value").isTextual()) {
String stringValue = root.get("value").asText();
return new AttributeEntry(name, stringValue);
} else {
return null; // or whatever
}
}
}
Because of this ambiguous type inconvenience, you will have to do some type checking throughout your code base.
You can then add this custom deserializer to your object mapper like so:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(AttributeEntry.class, new AttributeDeserializer());
objectMapper.registerModule(simpleModule);
Here's the AttributeEntry:
public class AttributeEntry {
private String name;
private Object value;
public AttributeEntry(String name, String value) {
this.name = name;
this.value = value;
}
public AttributeEntry(String name, ValueObject attributes) {
this.name = name;
this.value = attributes;
}
/* getters/setters */
}
Here's the ValueObject:
public class ValueObject {
private String key;
private String label;
/* getters/setters */
}
I have an Item class. There's an itemType field inside of that class which is of type ItemType.
roughly, something like this.
class Item
{
int id;
ItemType itemType;
}
class ItemType
{
String name;
int somethingElse;
}
When I am serializing an object of type Item using Jackson ObjectMapper, it serializes the object ItemType as a sub-object. Which is expected, but not what I want.
{
"id": 4,
"itemType": {
"name": "Coupon",
"somethingElse": 1
}
}
What I would like to do is to show the itemType's name field instead when serialized.
Something like below.
{
"id": 4,
"itemType": "Coupon"
}
Is there anyway to instruct Jackson to do so?
Check out #JsonValue annotation.
EDIT: like this:
class ItemType
{
#JsonValue
public String name;
public int somethingElse;
}
You need to create and use a custom serializer.
public class ItemTypeSerializer extends JsonSerializer<ItemType>
{
#Override
public void serialize(ItemType value, JsonGenerator jgen,
SerializerProvider provider)
throws IOException, JsonProcessingException
{
jgen.writeString(value.name);
}
}
#JsonSerialize(using = ItemTypeSerializer.class)
class ItemType
{
String name;
int somethingElse;
}
As OP only wants to serialize one field, you could also use the #JsonIdentityInfo and #JsonIdentityReference annotations:
class Item {
int id;
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="name")
#JsonIdentityReference(alwaysAsId=true)
ItemType itemType;
}
For more info, see How to serialize only the ID of a child with Jackson.
Perhaps a quick workaround is to add an extra getter on Item to return ItemType.name, and mark ItemType getter with #JsonIgnore?
To return simple string, you can use default ToStringSerializer without define any extra classes. But you have to define toString() method return this value only.
#JsonSerialize(using = ToStringSerializer.class)
class ItemType
{
String name;
int somethingElse;
public String toString(){ return this.name;}
}