Spring Hateoas: EntityModel _links rendered before content - spring-restcontroller

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)

Related

Kotlin parse 2 type of field(string, object) like on Swift

on IOS i have a code for parsing nested json. This json can contains string and object. It looks like this(Swift):
struct Place: Codable {
let value: [Dependent]?
}
enum Dependent: Codable {
case object(Place)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let objectVal = try container.decode(Place.self)
self = .object(objectVal)
} catch DecodingError.typeMismatch {
let stringVal = try container.decode(String.self)
self = .string(stringVal)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .object(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
But its to hard understand how i can create the same parser for Kotlin, i did code like this:
data class Place(
val value: List<Dependent>
)
sealed class Dependent {
data class OBJECT(val value: Place): Dependent()
data class STRING(val value: String): Dependent()
}
It doesnt work, i feel i missed something
UPD:
The josn looks like this:
{
"value": [
"123123123",
{"value": ["123123", "123123", {"value":["123"]}, "123"]}
]
}

Populate data classes with values from linkedhashmap

I have a linkedhashmap that has the following shape: <String, Subject>. The class Subject has the following fields:
class Subject {
var name: Boolean? = null
var lastname: Boolean? = null
var location: Boolean? = null
..
}
final_result =
"admin" -> Subject
"customer" -> Subject
etc.
I need to populate data classes that have the following format:
data class SubjectSummary(
val admin: SubjectData,
val customer: SubjectData
...
)
data class SubjectData(val details: DetailsData)
data class DetailsData(val name:String, val lastName:String ...)
Because I need to serialize the SubjectSummary class and get the following json format:
{
"admin": {
"details": {
"name": "",
"lastname": "",
...
}
}
"customer": {
"details": {
"name": "",
"lastname": "",
...
}
}
}
How do I assign the final_result map to match the SubjectSummary structure? I have done it with simple data classes, but when the fields within the data class are data classes, I'm not sure hot to populate it. Any guidance?
For simplicity I'm only showing a small example with a few fields.
If your goal with this transformation is just to be able to serialize with the given JSON format, then you don't need this SubjectSummary class. A Map<String, SubjectData> would be sufficient and probably more convenient to create when transforming from the initial map.
Also, it seems that DetailsData contains the same fields as Subject. If that's the case there is no need for an extra class either.
So in the end it seems you just need to create a Map<String, SubjectData where SubjectData could be defined as data class SubjectData(val details: Subject). You can transform your initial map pretty easily then:
val transformed = finalResult.mapValues { (_, subject) -> SubjectData(subject) }

Jackson parse JSON into Java Map

I have such JSON:
{
"list": [
{
"product": {
"id": 1123456,
"context": {
}
},
"items": [
]
},
and a code that reads it:
TypeReference<HashMap<String, Object>> typeRef
= new TypeReference<HashMap<String, Object>>() {};
InputStream inputStream = TypeReference.class.getResourceAsStream("/mocks/Docs.json");
Map<String, Object> map = mapper.readValue(inputStream, typeRef);
But I don't want the simple Map<String, Object>, I want to map into a map that looks like Map<String, MyRepresentation> map:
Is there a direct way to do it, or I need first to read it into Map<String, Object> and then manipulate it manually and fill the MyRepresentation object?
the JSON file structure doesn't correspond to the classes at all.
ConditionSummary looks like a type of contextData element accessible as docList[i].product.contextData if it had id, which is defined in the product element.
also, AccountManagerStatistics#map is not public and doesn't have #JsonProperty annotation, so it is out of json for now.
try creating the sample file first if you sure the classes represent the truth:
Map<String, AccountManagerStatistics> map = createStubData();
new ObjectMapper().writerFor(new TypeReference<Map<String, AccountManagerStatistics>>() {}).writeValueAsString(map)
or try to modify your classes to match the data,
which is probably what should be done here.
then you could start from the top and introduce a proper class instead of using Map<String, X>,
BTW no need for HashMap in TypeReference:
//#XmlRootElement
public class Root {
#JsonProperty("docList") //or #XmlElement("docList")
public final List<Doc> docs;
...
#ConstructorProperties({ "docs", ... })
public Root(List<Doc> docs, ...) {
this.docs = List.copyOf(docs);
...
}
}

How to deserialize JSON having references to abstract types in 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.

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.