How to deserialize JSON having references to abstract types in Jackson - jackson

I have an issue with object references to abstract classes and JSON serialization and deserialization. The abstracted issue looks like this:
I have a graph consisting of nodes and edges. Each edge connects two nodes. Nodes can be of flavor red and green. Therefore, there is an abstract class Node and two derived classes RedNode and GreenNode. A Node takes an id (#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")):
#JsonSubTypes({
#JsonSubTypes.Type(value = GreenNode.class, name = "GreenNode"),
#JsonSubTypes.Type(value = RedNode.class, name = "RedNode")
})
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public abstract class Node {
public String id;
}
public class RedNode extends Node {
// ...
}
public class GreenNode extends Node {
// ...
}
An Edge has a source and a target of type Node, which are serialized as references (#JsonIdentityReference(alwaysAsId = true)):
public class Edge {
#JsonIdentityReference(alwaysAsId = true)
public Node source;
#JsonIdentityReference(alwaysAsId = true)
public Node target;
}
The graph is defined as follows:
public class Graph {
public List<GreenNode> greenNodes = new ArrayList();
public List<RedNode> redNodes = new ArrayList();
public List<Edge> edges = new ArrayList();
}
An example JSON looks as follows:
{
"greenNodes" : [ {
"id" : "g",
"content" : "green g",
"greenProperty" : "green"
} ],
"redNodes" : [ {
"id" : "r",
"content" : "red r",
"redProperty" : "red"
} ],
"edges" : [ {
"source" : "g",
"target" : "r"
} ]
}
Using an ObjectMapper cannot read this:
Can not construct instance of com.github.koppor.jsonidentityissue.model.Node: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
The error location is "line: 13, column: 16". Thus, it is hit at the id of the edge. The nodes themselves are properly serialized.
A workaround is to add type information in the json:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
public abstract class Node {
Then, everything works:
{
"greenNodes" : [ {
"id" : "g",
"type" : "GreenNode",
"content" : "green g",
"greenProperty" : "green"
} ],
"redNodes" : [ {
"id" : "r",
"type" : "RedNode",
"content" : "red r",
"redProperty" : "red"
} ],
"edges" : [ {
"source" : "g",
"target" : "r"
} ]
}
Then, everything works.
Is it really necessary to include type information in the referenced objects to have the reference working? Without the type information, a graph with red and green nodes (and no edges) can be loaded. After an edge comes in, it can't. However, the JSON of an edge contains an id only. The referenced objects are already parsed.
I really like to get rid off the #JsonTypeInfo annotation. Is there a way to have a clean JSON?
The full example is made available at https://github.com/koppor/jackson-jsonidentityreference-issue/tree/issue.

The current solution is to include fake type information. Full code at https://github.com/koppor/jackson-jsonidentityreference-issue.
The Node gets an existing property type, which is not written:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type")
public abstract class Node {
#JsonIgnore
public abstract String getType();
}
Each subclass specifies itself as defaultImpl and provides an implementation of getType:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type",
defaultImpl=GreenNode.class)
public class GreenNode extends Node {
#Override
public String getType() {
return "GreeNode";
}
}
This way, the JSON remains clean, but Jackson can resolve the id reference without any issues.

Related

Kotlin sealed class

I have below class, I would like to make this class a sealed class. Can you please help me as I am new to Kotlin.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = A:class, name = "PIZZA"),
JsonSubTypes.Type(value = B::class, name = "DONUT"),
JsonSubTypes.Type(value = C::class, name = "ICECREAM"),
JsonSubTypes.Type(value = D::class, name = "CHOCOLATE"),
)
open class food (var type: foodType, var quantity : String) {
open val taste : String=""
}
How to make this a sealed class perhaps a subclass of a sealed class, and how to instantiate it?
The foodType is enum class
enum class foodType {
PIZZA,
DONUT,
ICECREAM,
CHOCOLATE
}
I have the following based on the other post, but I am confused on passing the right parameters. Can someone help me understand what parameter I need to pass??
sealed class food (var type: foodType, var quantity: String) {
class favFood(taste: String): food(?, ?)
}
What is a sealed class ?
When you create a sealed class, you only allow the implementations you
created, just like for an enum (Only the constants you added are allowed). Once the module is compiled, you can't add any additional implementation anymore (in opposite to an open class).
Here is the link to the Kotlin documentation about sealed classes : https://kotlinlang.org/docs/sealed-classes.html
Sealed classes are interesting when you want to restrict the implementations
to a strict proposition. It can be the case with your use case, to restrict the jsonSubTypes you allow (others wouldn't be mapped).
How to transform an open class to a sealed class ?
So to transform your open class to a sealed class, you generally just need to change the keyword open to sealed. However, you also need to understand how the inheritance mechanism work with sealed classes.
For your example
With JsonSubType, you just need to map the property type to an implementation of your sealed class using a constant of your choice.
Also, you have to provide the values to your sealed class' properties when you extend it, so when you create your implementations.
In the next example, you can find how to give a value to your sealed class properties and what will be the result when you map it to json using JSonSubType :
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = Pizza::class, name = "Pizza"),
JsonSubTypes.Type(value = Donut::class, name = "DonutDesert"), // As you can see, name is a value you give, not always need to be the class name
JsonSubTypes.Type(value = IceCream::class, name = "IceCream")
)
sealed class Food(val taste: String)
class Pizza(val size: PizzaSize, taste: String) : Food(taste) {
enum class PizzaSize {
SMALL,
MEDIUM,
LARGE
}
}
class Donut(val glaze: String, taste: String) : Food(taste)
class IceCream(val servings: Int, taste: String) : Food(taste)
class Basket(foods: List<Food>)
/* If you map a Basket to JSON, it will give you this :
{ foods: [
{ "type": "Pizza", "size": "MEDIUM", "taste": "Hawaii" },
{ "type": "DonutDesert", "glaze": "Sugar & Marshmallows", "taste" : "chocolate"},
{ "type": "IceCream", "servings": 3, "taste": "Strawberry" }
]}
*/

Spring Hateoas: EntityModel _links rendered before content

This is a weird problem to describe since it's no actually a problem in the technical sense but still makes me curious enough to ask about it:
I created a #RestController that returns ResponseEntity<EntityModel<?>>. I build the EntityModel and attach a self link built with linkTo and methodOn. Now for some reason, the output looks like this:
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/points/knx/office_light"
}
},
"labels" : {
"name" : "Light",
"room" : "Office"
},
"access" : [ "READ", "WRITE" ],
"type" : "SwitchPoint",
"state" : "OFF"
}
Contrary to other rest services I have build, the "_link" gets rendered at the top not at the bottom. Any ideas why?
#GetMapping("{ext}/{id}")
public ResponseEntity<EntityModel<Map<String, Object>>> oneByExt(#PathVariable String ext,
#PathVariable String id) {
EntityModel<Map<String, Object>> point = client.getPoint(ext, id);
return new ResponseEntity<>(localToGlobal(ext, point), HttpStatus.OK);
}
private <T> EntityModel<T> localToGlobal(String ext, EntityModel<T> model) {
ComposedId id = ComposedId.fromEntityModel(ext, model);
Link newSelfLink = linkTo(methodOn(PointController.class).oneByExt(id.getExtension(), id.getIdentifier()))
.withSelfRel();
EntityModel<T> newModel = EntityModel.of(model.getContent());
newModel.add(newSelfLink);
return newModel;
}
It's probably due to the Map, I'm assuming you using something like HashMap which has no guarantee of iteration order. Try change it to a LinkedHashMap and see what happens (should print the values in the order they were added to the map)

Can't POST a collection

I have a simple Entity with a single collection mapped.
#Entity
public class Appointment Identifiable<Integer> {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonIgnore
private Integer id;
#Column(name="TRAK_NBR")
private String trackNumber;
#OneToMany(fetch =FetchType.EAGER, cascade= CascadeType.ALL)
#JoinColumn(name="CNSM_APT_VER_WRK_I", nullable = false)
private Set<Product> products = new HashSet<Product>();
}
#Entity
public class Product implements Identifiable<Integer> {
#Id
#Column(name = "CNSM_PRD_VER_WRK_I")
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonIgnore
private Integer id;
#Column(name = "PRD_MDL_NBR")
private String model;
#Column(name = "PRD_SPEC_DSC")
private String description;
}
In my application when I only include a PagingAndSortingRepository for Appointment. I can call the POST command with the following payload.
{
"trackNumber" : "XYZ123",
"products": [
{"model" : "MODEL",
"description" : "NAME"
}]
}
When I add a PagingAndSortingRepository for Product and try the same POST I get the following error message.
{
"cause" : {
"cause" : {
"cause" : null,
"message" : null
},
"message" : "(was java.lang.NullPointerException) (through reference chain: com..model.Appointment[\"products\"])"
},
"message" : "Could not read JSON: (was java.lang.NullPointerException) (through reference chain: com.model.Appointment[\"products\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.model.AppointmentVerification[\"products\"])"
}
My GET payload with both Repositories returns this. This is my desired format. The link to products should be included
{
"trackNumber" : "XYZ123",
"_links" : {
"self" : {
"href" : "http://localhost:8080/consumerappointment/appointments/70"
},
"products" : {
"href" : "http://localhost:8080/consumerappointment/appointments/70/products"
}
}
With only the Appointment repository I get the following payload and can post the list of products.
{
"trackNumber" : "XYZ123",
"products" : [ {
"model" : "MODEL",
"description" : "NAME",
} ],
"_links" : {
"self" : {
"href" : "http://localhost:8080/consumerappointment/appointments/1"
}
}
}
Let's take a step back and make sure you understand what's happening here: if a repository is detected, Spring Data REST exposes a dedicated set of resources for it to manage the aggregates handled by the repository via HTTP. Thus, if you have repositories for multiple entities related to each other, the relationship is represented as a link. This is why you see the products inlined with only the AppointmentRepository in place and the products link in place once you create a ProductRepository.
If you want to expose both repositories as resources, you need to hand the URIs of the Product instances in the payload for the POST to create an Appointment. That means, instead of posting this:
{ "trackNumber" : "XYZ123",
"products": [
{ "model" : "MODEL",
"description" : "NAME"
}
]
}
you'd create a Product first:
POST /products
{ "model" : "MODEL",
"description" : "NAME" }
201 Created
Location: …/products/4711
And then hand the ID of the product to the Appointment payload:
{ "trackNumber" : "XYZ123",
"products": [ "…/products/4711" ]}
In case you don't want any of this (no resources exposed for Product in the first place, use #RepositoryRestResource(exported = false) on PersonRepository. That would still leave you with the bean instance created for the repo, but no resources exported and the resource exposed for Appointments back to inlining related Products.

Ignore Jackson polymorphic type mapping

I'm using #JsonTypeInfo and #JsonSubTypes to map parsing subclasses based on a given property. Here's a contrived example of my sample JSON that I want to parse.
{ "animals": [
{ "type" : "dog", "name" : "spike" }
,{ "type" : "cat", "name" : "fluffy" }
]}
Using this as the class
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME
,include = JsonTypeInfo.As.PROPERTY
,property = "type")
#JsonSubTypes({
#Type(value = Cat.class, name = "cat"),
#Type(value = Dog.class, name = "dog") })
abstract class Animal {
public String name;
{
class Dog extends Animal { }
class Cat extends Animal { }
However, the problem occurs when the JSON contains type that I would want to ignore. For example, if I have a new type "pig" that I don't really want to deserialize as an object:
{ "animals": [
{ "type" : "dog", "name" : "spike" }
,{ "type" : "cat", "name" : "fluffy" }
,{ "type" : "pig", "name" : "babe" }
]}
and try to parse it, it will give me this error:
Could not resolve type id 'pig' into a subtype of [simple type, class
Animal]
How can I fix it so that I can map only those animals of type 'dog' and 'cat', and ignore everything else?
You can avoid exception by setting the JsonTypeInfo.defaultImpl annotation attibute to java.lang.Void or NoClass depending on the Jackson version you are using.
Here is an example:
public class JacksonUnknownType {
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type", defaultImpl = Void.class)
#JsonSubTypes({
#JsonSubTypes.Type(value = Cat.class, name = "cat"),
#JsonSubTypes.Type(value = Dog.class, name = "dog")})
public abstract static class Animal {
public String name;
#Override
public String toString() {
return getClass().getName() + " :: " + name;
}
}
public static class Dog extends Animal {
}
public static class Cat extends Animal {
}
public static final String JSON = "[\n" +
" { \"type\" : \"dog\", \"name\" : \"spike\" }\n" +
" ,{ \"type\" : \"cat\", \"name\" : \"fluffy\" }\n" +
" ,{ \"type\" : \"pig\", \"name\" : \"babe\" }\n" +
"]";
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
List<Animal> value = mapper.readValue(JSON, new TypeReference<List<Animal>>() {});
System.out.println(value);
}
}
Output:
[stackoverflow.JacksonUnknownType$Dog :: spike, stackoverflow.JacksonUnknownType$Cat :: fluffy, null, null, null]
Note that the resulting collection contains 3 null items instead of 1. This may be a bug in Jackson but it is easy to tolerate.

How to unwrap single item array and extract value field into one simple field?

I have a JSON document similar to the following:
{
"aaa": [
{
"value": "wewfewfew"
}
],
"bbb": [
{
"value": "wefwefw"
}
]
}
I need to deserialize this into something more clean such as:
public class MyEntity{
private String aaa;
private String bbb;
}
What's the best way to unwrap each array and extract the "value" field on deserialization? Just custom setters? Or is there a nicer way?
For completeness, if you use jackson, you can enable the deserialization feature UNWRAP_SINGLE_VALUE_ARRAYS.
To do that, you have to enable it for the ObjectMapper like so:
ObjectMapper objMapper = new ObjectMapper()
.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
With that, you can just read the class as you are being used to in Jackson.
For example, assuming the class Person:
public class Person {
private String name;
// assume getter, setter et al.
}
and a json personJson:
{
"name" : [
"John Doe"
]
}
We can deserialize it via:
ObjectMapper objMapper = new ObjectMapper()
.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
Person p = objMapper.readValue(personJson, Person.class);
Quick solution with Gson is to use a JsonDeserializer like this:
package stackoverflow.questions.q17853533;
import java.lang.reflect.Type;
import com.google.gson.*;
public class MyEntityDeserializer implements JsonDeserializer<MyEntity> {
public MyEntity deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
String aaa = json.getAsJsonObject().getAsJsonArray("aaa").get(0)
.getAsJsonObject().get("value").getAsString();
String bbb = json.getAsJsonObject().getAsJsonArray("bbb").get(0)
.getAsJsonObject().get("value").getAsString();
return new MyEntity(aaa, bbb);
}
}
and then use it when parsing:
package stackoverflow.questions.q17853533;
import com.google.gson.*;
public class Q17853533 {
public static void main(String[] arg) {
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(MyEntity.class, new MyEntityDeserializer());
String testString = "{ \"aaa\": [{\"value\": \"wewfewfew\" } ], \"bbb\": [ {\"value\": \"wefwefw\" } ] }";
Gson gson = builder.create();
MyEntity entity= gson.fromJson(testString, MyEntity.class);
System.out.println(entity);
}
}