how to write jackson deserializer based on property in json - java

I want to write json deserializer on class Type so that when Type is deserialized from given json based on name it maps value (of type interface Being) to its current implementation based on some factory method that returns correct class name based on name, and populates remaining class without any explicit deserialization and without creating object of TigerBeing or HumanBeing explicitly using new.
I tried to use #jsonCreator but there i have to initialize entire HumanBeing or TigerBeing using new and passing all json in constructor. I need auto mapping for types further used as further pojo can be quite complex.
{type:[{
"name": "Human",
"value": {
"height":6,
"weight":100,
"languages":["spanish","english"]
}
},
{
"name":"Tiger",
"value":{
"extinct":1,
"found":["Asia", "America", "Europe", "Africa"]
}
}
]}
I have:
public class Type {
String name;
Being value;
}
public interface Being {
}
public class TigerBeing implements Being {
Integer extinct;
String[] found;
}
public class HumanBeing implement Being {
Integer height;
Integer weight;
String[] languages;
}

import java.io.IOException;
public class BeingDeserializer extends JsonDeserializer<Being> {
#Override
public Expertise deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonMappingException {
JsonNode node = jp.getCodec().readTree(jp);
String beingName = node.get("name").asText();
JsonNode valueNode = node.get("value");
BeingName beingByName = ExpertiseName.getBeingByName(beingName);
if(beingByName ==null) {
throw new JsonMappingException("Invalid Being " + beingName);
}
Being being = JsonUtils.getObjectFromJsonNode(valueNode,
ExpertiseFactory.getExpertise(beingByName).getClass());
return being;
}
}
In this way I was able to solve the above problem.

Related

Jackson deserialization interface on multiple types

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);
}
}

How do I deserialize a string as a custom object with Jackson?

I have this JSON that I want to parse:
{
"name": "john",
}
I must use the following hierarchy. The classes are immutable and I must access them by the static factory method (this is imperative, so it makes no sense to suggest modifications to either Name or Person).
class Name {
static Name valueOf(String name) {...}
private Name() {}
String name();
}
class Person {
static Person create(Name name) {...}
private Person() {...}
Name name();
}
To that end, I want to deserialize a Person with Jackson, so I wrote this:
public class NameJsonDeserializer extends JsonDeserializer<Name> {
#Override public Name deserialize(JsonParser parser, DeserializationContext context) {
var tree = parser.getCodec().readTree(parser);
var name = tree.asToken().asString();
return Name.valueOf(name);
}
}
public class PersonJsonDeserializer extends JsonDeserializer<Person> {
#Override public Person deserialize(JsonParser parser, DeserializationContext context) {
var tree = parser.getCodec().readTree(parser);
var name = (ObjectNode) tree.get("name");
return Person.create(name);
}
}
But of course, this doesn't work. It doesn't even compile.
I know I can write something similar to the following, but Name is used all over the place, and not always within a Person, so I really need a separate deserializer for Name.
var tree = parser.getCodec().readTree(parser);
var name = (TextNode) tree.get("name");
return Person.create(Name.valueOf(name.asText()));
How can I deserialize without having recourse to intermediary POJOs?
I must use parser.getValueAsString() for NameDeserializer and codec.treeToValue() for PersonDeserializer:
public class NameJsonDeserializer extends JsonDeserializer<Name> {
#Override public Name deserialize(JsonParser parser, DeserializationContext context) {
var name = parser.getValueAsString();
return Name.valueOf(name);
}
}
public class PersonJsonDeserializer extends JsonDeserializer<Person> {
#Override public Person deserialize(JsonParser parser, DeserializationContext context) {
var codec = parser.getCodec();
var tree = codec.readTree(parser);
var name = codec.treeToValue(tree.get("name"), Name.class);
return Person.create(name);
}
}
If you can modify Person, you could use #JsonCreator:
#JsonCreator
public static Person create(#JsonProperty("name") String name) {
Name nameInstance = Name.valueOf(name);
return new Person(nameInstance);
}
This will consume that JSON properly, mapping the "name" property to the first param of the method.
If you cannot modify the class you can use the mixin approach, creating the mixin:
interface PersonMixin {
#JsonCreator
public static Person create(#JsonProperty("name") Name name) {return null;}
}
and register it in the mapper:
mapper.addMixIn(Person.class, PersonMixin.class);

JSON to Java object with Random Key

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.

Decide class for Jackson deserialisation based on JSON value of a specific key [duplicate]

I want to write json deserializer on class Type so that when Type is deserialized from given json based on name it maps value (of type interface Being) to its current implementation based on some factory method that returns correct class name based on name, and populates remaining class without any explicit deserialization and without creating object of TigerBeing or HumanBeing explicitly using new.
I tried to use #jsonCreator but there i have to initialize entire HumanBeing or TigerBeing using new and passing all json in constructor. I need auto mapping for types further used as further pojo can be quite complex.
{type:[{
"name": "Human",
"value": {
"height":6,
"weight":100,
"languages":["spanish","english"]
}
},
{
"name":"Tiger",
"value":{
"extinct":1,
"found":["Asia", "America", "Europe", "Africa"]
}
}
]}
I have:
public class Type {
String name;
Being value;
}
public interface Being {
}
public class TigerBeing implements Being {
Integer extinct;
String[] found;
}
public class HumanBeing implement Being {
Integer height;
Integer weight;
String[] languages;
}
import java.io.IOException;
public class BeingDeserializer extends JsonDeserializer<Being> {
#Override
public Expertise deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonMappingException {
JsonNode node = jp.getCodec().readTree(jp);
String beingName = node.get("name").asText();
JsonNode valueNode = node.get("value");
BeingName beingByName = ExpertiseName.getBeingByName(beingName);
if(beingByName ==null) {
throw new JsonMappingException("Invalid Being " + beingName);
}
Being being = JsonUtils.getObjectFromJsonNode(valueNode,
ExpertiseFactory.getExpertise(beingByName).getClass());
return being;
}
}
In this way I was able to solve the above problem.

Json Deserialization in Java /w Jackson of mixed types, contained in one array

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 */
}

Categories

Resources