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

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

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

Jackson Custom Deserializer for polymorphic objects and String literals as defaults

I'd like to deserialize an object from YAML with the following properties, using Jackson in a Spring Boot application:
Abstract class Vehicle, implemented by Boat and Car
For simplicity, imagine both have a name, but only Boat has also a seaworthy property, while Car has a top-speed.
mode-of-transport:
type: boat
name: 'SS Boatface'
seaworthy: true
----
mode-of-transport:
type: car`
name: 'KITT'
top-speed: 123
This all works fine in my annotated subclasses using #JsonTypeInfo and #JsonSubTypes!
Now, I'd like to create a shorthand using only a String value, which should create a Car by default with that name:
mode-of-transport: 'KITT'
I tried creating my own custom serializer, but got stuck on most of the relevant details. Please help me fill this in, if this is the right approach:
public class VehicleDeserializer extends StdDeserializer<Merger> {
/* Constructors here */
#Override
public Vehicle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (/* it is an OBJECT */){
// Use the default polymorphic deserializer
} else if (/* it is a STRING */) {
Car car = new Car();
car.setName( /* the String value */ );
return car;
}
return ???; /* what to return here? */
}
}
I found these 2 answers for inspiration, but it looks like combining it with polymorphic types makes it more difficult: How do I call the default deserializer from a custom deserializer in Jackson and Deserialize to String or Object using Jackson
A few things are different than the solutions offered in those questions:
I am processing YAML, not JSON. Not sure about the subtle differences there.
I have no problem hardcoding the 'default' type for Strings inside my Deserializer, hopefully making it simpler.
This was actually easier than I thought to solve it. I got it working using the following:
Custom deserializer implementation:
public class VehicleDeserializer extends StdDeserializer<Vehicle> {
public VehicleDeserializer() {
super(Vehicle.class);
}
#Override
public Vehicle deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.currentToken() == JsonToken.VALUE_STRING) {
Car car = new Car();
car.setName(jp.readValueAs(String.class));
return car;
}
return jp.readValueAs(Vehicle.class);
}
}
To avoid circular dependencies and to make the custom deserializer work with the polymorphic #JsonTypeInfo and #JsonSubTypes annotations I kept those annotations on the class level of Vehicle, but put the following annotations on the container object I am deserializing:
public class Transport {
#JsonDeserialize(using = VehicleDeserializer.class)
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
private Vehicle modeOfTransport;
// Getter, setters
}
This means that by default a Vehicle is deserialized as a polymorphic object, unless explicitly specified to deserialize it using my custom deserializer. This deserializer will then in turn defer to the polymorphism if the input is not a String.
Hopefully this will help someone running into this issue :)
So there is a solution that requires you to handle the jackson errors using a DeserializationProblemHandler (since you want to parse the same type using different inputs, this is not achieved easily using regular means):
public class MyTest {
#Test
public void doTest() throws JsonParseException, JsonMappingException, IOException {
final ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(final DeserializationContext ctxt, final Class<?> instClass, final JsonParser p, final String msg) throws IOException {
if (instClass.equals(Car.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return new Car();
}
}
return NOT_HANDLED;
}
#Override
public JavaType handleMissingTypeId(final DeserializationContext ctxt, final JavaType baseType, final TypeIdResolver idResolver, final String failureMsg) throws IOException {
// if (baseType.isTypeOrSubTypeOf(Vehicle.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return TypeFactory.defaultInstance().constructType(Car.class);
}
return super.handleMissingTypeId(ctxt, baseType, idResolver, failureMsg);
}
});
final Container objectValue = om.readValue(getObjectJson(), Container.class);
assertTrue(objectValue.getModeOfTransport() instanceof Car);
final Container stringValue = om.readValue(getStringJson(), Container.class);
assertTrue(stringValue.getModeOfTransport() instanceof Car);
}
private String getObjectJson() {
return "{ \"modeOfTransport\": { \"type\": \"car\", \"name\": \"KITT\", \"speed\": 1}}";
}
private String getStringJson() {
return "{ \"modeOfTransport\": \"KITT\"}";
}
}
class Container {
private Vehicle modeOfTransport;
public Vehicle getModeOfTransport() {
return modeOfTransport;
}
public void setModeOfTransport(final Vehicle modeOfTransport) {
this.modeOfTransport = modeOfTransport;
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
#JsonSubTypes({
#Type(name = "car", value = Car.class)
})
abstract class Vehicle {
protected String type;
protected String name;
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
#JsonTypeName("car")
class Car extends Vehicle {
private int speed;
public int getSpeed() {
return speed;
}
public void setSpeed(final int speed) {
this.speed = speed;
}
}
Note that I used JSON, not YAML, and you need to add your other subtypes as well.

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

how to write jackson deserializer based on property in json

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.

Categories

Resources