I wonder what's the recommended way to generate POJOs for Json schemas with "anyOf" fields?
For example, given the following json schemas:
hobby.json
{
"anyOf": [
{ "type": {"$ref": "./exercise.json" } },
{ "type": {"$ref": "./music.json" } }
]
}
exercise.json
{
"type": "object"
"properties" {
"hobbyType": {"type": "string"}
"exerciseName": { "type": "string" },
"timeSpent": { "type": "number" },
"place": { "type": "string" }
}
}
music.json
{
"type": "object"
"properties" {
"hobbyType": {"type": "string"}
"instrument": { "type": "string" },
"timeSpent": { "type": "number" }
}
}
How could I generate a POJO for Hobby.java with Jackson?
I think there are two approaches that seem natural:
One would be to generate a class hierarchy Hobby with the common field timeSpent and Music / Exercise being subclasses with their specific fields.
The other would be to "union" the fields into a single class Hobby.
Both are semantically incorrect meaning that you can come up with cases where JSON schema validates correctly but Jackson throws an error or the information is missing in the POJO due to an omitted field.
So I think the best approach here would be to resort to Map<String, Object> instead of pojos.
So for example if a Person has a hobby the Person POJO could be:
class Person {
String name;
...
Map<String, Object> hobby;
or List<Map<String, Object> hobbies> if one can have multiple hobbies.
The approach I ended up taking is using polymorphic marshaling/unmarshaling functionality provided by Jackson.
Specifically -
Make hobby to be an interface and annotate it with #JsonTypeInfo and #JsonSubTypes
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "hobbyType",
include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true
)
#JsonSubTypes({
#Type(value = Exercise.class, name = "exercise"),
#Type(value = Music.class, name = "music")
})
public interface Hobby {
}
Create Exercise.java and Music.java that implements this interface
#Builder
#Data
#AllArgsConstructor
public class Exercise implements Hobby {
#JsonProperty("hobbyType")
#Builder.Default
#NonNull
private final String hobbyType = "exercise";
#JsonProperty("exerciseName")
private String exerciseName;
#JsonProperty("place")
private String place;
//... (other fields)
}
Use Hobby for serialization and deserialization.
// create a Hobby object
Hobby exercise = Exercise.builder().exerciseName("swimming").place("swimmingPool").build();
// serialization
String serializedHobby = new ObjectMapper.writeValueAsString(exercise)
/**
serializedHobby looks like this ->
{
"hobbyType": "exercise",
"exerciseName": "swimming",
"place": "swimmingPool"
}
*/
// deserialization
Hobby deserializedObject = new ObjectMapper.readValue(jsonString, Hobby.class)
// deserializedObject.getClass() would return Exercise.java or Music.java based on the hobbyType
Ref: https://www.baeldung.com/jackson-inheritance
Related
I am creating a JSON mapper that will create JSON schema for my JPA database classes. I am using mbknor-jackson-jsonSchema that works great, but I need to serialize my subclasses as id only.
So far my structure is:
public class ModelPojo{
private List<Table> table;
private List<Table2> table2;
}
Both table classes look similar to this:
#Table(name = "TABLE")
public class Table extends BaseEntity {
#Column(name = "SMTH")
private String smth;
#JoinColumn(name = "TABLE2")
private Table2 table2; //now this is where is the problem
}
BaseEntity contains integer id field.
The question is, is there a way to write custom std serializer that serializes table entity to have property id_table2: "integer", but not the whole object?
I tried to override serialize method for StdSerializer<BaseEntity> which does not work, it is not called when creating the schema
Edit: I now get
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Model Pojo",
"type": "object",
"additionalProperties": false,
"properties": {
"Table": {
"type": "array",
"items": {
"$ref": "#/definitions/Table"
}
}
"Table2": {
"type": "array",
"items": {
"$ref": "#/definitions/Table2"
}
}
},
"definitions": {
"Table": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "integer"
},
"table2": {
"$ref": "#/definitions/Table2"
},
"smth": {
"type": "string"
}
}
},
"Table2": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "integer"
},
"foo": {
"type": "string"
}
}
}
}
}
and i want to change
"table2": {
"$ref": "#/definitions/Table2"
},
to
"table2_id": {
"type": "integer"
},
The structure of joined tables is much more complex, so I am trying to not manually add #JsonIgnore and changing it manually, but write some type of serializer that retypes the child instances of BaseEntity to id, hope it's understandable.
If you want to ignore all properties of your domain entities Table and Table2 apart from id (inherited from BaseEntity) while serializing ModelPojo but at the same time you need all the properties Table and Table2 to be reflected while serializing them as standalone objects, you can introduce a method inside ModelPojo which to instruct the Jackson how to dial with this class.
public class ModelPojo {
private List<Table> table;
private List<Table2> table2;
public List<Map<String, Integer>> getTable() {
return extractIds(table);
}
public List<Map<String, Integer>> getTable2() {
return extractIds(table2);
}
private List<Map<String, Integer>> extractIds(List<? extends BaseEntity> list) {
return list.stream().map(t -> Map.of("id", t.getId())).toList();
}
}
In the absence of real getters, getTable() and getTable2() would be used by Jackson. No data-binding annotations required.
But you might need normal getters, that's understandable.
In such case, the approach described above can be improved by introducing a single method returning a Map (as a replacement of getTable() and getTable2()) annotated with #JsonAnyGetter (to make Jackson aware of this method) and #JsonUnwrapped (to flatten the contents of this map).
The fields table and table2 should be annotated with #JsonIgnore.
public class ModelPojo {
#JsonIgnore
private List<Table> table;
#JsonIgnore
private List<Table2> table2;
#JsonAnyGetter
#JsonUnwrapped
public Map<String, List<Map<String, Integer>>> getAll() {
return Map.of(
"table", extractIds(table),
"table2", List.of()
);
}
private List<Map<String, Integer>> extractIds(List<? extends BaseEntity> list) {
return list.stream().map(t -> Map.of("id", t.getId())).toList();
}
// getters
}
I am trying to not manualy add #JsonIgnore and changing it manually, but write some type of serializer that retypes the child instances of BaseEntity to id
Sure, you can implement a custom serializer for these fields as well.
For that, you need extend JsonSerializer class and implement its abstract method serialize().
public class TableSerializer extends JsonSerializer<List<? extends BaseEntity>> {
#Override
public void serialize(List<? extends BaseEntity> list,
JsonGenerator gen,
SerializerProvider serializers)
throws IOException {
gen.writeObject(extractIds(list));
}
private List<Map<String, Integer>> extractIds(List<? extends BaseEntity> list) {
return list.stream().map(t -> Map.of("id", t.getId())).toList();
}
}
A now we need to instruct Jackson to apply this serializer by specifying it as a value of using attribute of the #JsonSerialize annotation.
public class ModelPojo {
#JsonSerialize(using = TableSerializer.class)
private List<Table> table;
#JsonSerialize(using = TableSerializer.class)
private List<Table2> table2;
// getters
}
I made some changes, I changed JSON serialization framework to victools/jsonschema-generator, which allowed me to heavily modify its serialization
I then serialized with customDefinitionProvider. It helped me to change JsonNode names and most importantly I could chose what I want to serialize through code not via Json tags.
Thanks #Carsten for great work :)
In table2 you can write #JsonIgnore on the top field which you do not want to serialize it will not send it in json response. You can do it for all other fields than id
I have a json
{
"params": [
{
"key": "path",
"options": {
"string": {
"prefix": "test_pref"
}
},
"default": {
"url": ""
}
}
]}
I have the following POJO class, where i want to map inner objects like option.string.prefix in json to prefix in Params POJO class.
#Data
#Accessors(chain = true)
public class Data {
private List<Params> params;
}
#Data
#Accessors(chain = true)
public class Params {
private String key;
#JsonProperty("options.string.prefix")
private String prefix;
#JsonProperty("default.url")
private String url;
}
Is there any Jackson annotation that helps me do this without #JsonProperty?
The is #JsonGetter which is an alternative to #JsonProperty You can read a very nice article on the topic here: Jackson Annotation Examples
I am stuck deserializing a JSON using Jackson.
JSON objects:
{
"animal": {
"header": {
"id": "1",
"type": "dog"
}
"description": {
"color":"black",
"sound": "bark"
"loyal": true
}
}
{
"animal": {
"header": {
"id": "2",
"type": "cat"
}
"description": {
"color": "white",
"sound": "meow",
"sleepDuration": 14
}
}
{
"plant": {
"header": {
"id": "5",
"type": "rose"
}
"description": {
"color": "red",
"smell": "good"
}
}
Java Classes:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.WRAPPER_OBJECT
)
#JsonSubTypes({
#JsonSubTypes.Type( value = Animal.class, name = "animal" ),
#JsonSubTypes.Type( value = Plant.class, name = "plant")
})
#JsonRootName("species")
public abstract class Species {
public Header header;
public Description description;
//...
}
#JsonRootName("animal")
public class Animal extends Species {
//...
}
#JsonRootName("plant")
#Getter
public class Plant extends Species {
//...
}
#JsonTypeInfo(
// ????????? Not sure if there is an annotation to help with this.
)
#JsonSubTypes({
#JsonSubTypes.Type(name = "DogDescription", value = DogDescription.class),
#JsonSubTypes.Type(name = "RoseDescription", value = RoseDescription.class),
#JsonSubTypes.Type(name = "CatDescription", value = CatDescription.class)})
public abstract class Description {
private String color;
}
#JsonRootName("description")
public class DogDescription {
private Sound sound;
private boolean loyal;
}
#JsonRootName("description")
public class CatDescription {
private Sound sound;
private int sleepDuration;
}
The serialization of animal succeeded. However, I have issue on how to serialize the description using type information in the header.
use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, doesn't work since the type property is not under the Description Object. I have tried also to do a customTypeId resolver as described in this post : Jackson polymorphic deserialization with type property that is nested in object. It didn't work. I think because once the species serialization is performed, Jackson would pass the JSON tree of Description. I couldn't find a way to access the type in the header.
I am wondering if there is a solution to this problem without using a custom serializer.
I want to map field in Entity like #CompletionField, but with contexts, cause now Completion includes String[] and int weight field. I want to filtering completion in index.
#Document(indexName = "compl_index")
public class ComplIndex {
#CompletionField
private Completion suggestions;
}
When I write this class, I have a simple completion with string array and weight, but I want to map entity like this, and work with contexts. I try to solve this problem - write a new entity with fields type, contexts etc and annotate with Mapping, CompletionFieldMapper throw exception "contexts field is not supported for field:...
"name": {
"type": "completion",
"contexts": [
{
"name": "year",
"type": "category",
"path": "year"
}
]
},
"year": {
"type": "text"
}
It is already supported, you can find the example here DATAES-536. For lower version, you need to write a custom completion model and use field #Mapping instead of #CompletionField.
public class CustomCompletion {
private String[] input;
private Map<String, List<String>> contexts;
private Integer weight;
private CustomCompletion() {
// required by mapper to instantiate object
}
public CustomCompletion(String[] input) {
this.input = input;
}
// Setter getter
}
#Document(indexName = "compl_index")
public class ComplIndex {
#Mapping(mappingPath = "/mapping/compl-index-suggestions.json")
private CustomCompletion suggestions;
}
compl-index-suggestions.json
{
"type": "completion",
"contexts": [
{
"name": "year",
"type": "category"
}
]
}
Lets say we have the following JSON example:
{
"teachers": [{
"id": "abc",
"payment": 10,
"name": "xyz",
"clases": ["1", "3"]
}, {
"id": "qwe",
"payment": 12,
"name": "xcv",
"classes": ["1", "2"]
}],
"classes": [{
"id": "1",
"room": 7
}, {
"id": "2",
"room": 1
}, {
"id": "3",
"room": 2
}]
}
I would like to deserialize it to Java objects (getters/setters ommited):
class Teacher {
private String id;
private double payment;
private String name;
private List<CLassRoom> classRooms;
}
class ClassRoom {
private String id;
private int room;
}
As you see, we have a references here. I know I can deserialize it with Jackson (and would like to) but the problem is that I cannot touch DTO itself (so annotations are not possible, would also like to avoid wrappers (many classes)). Also, it would be nice if the "configuration" of deserialization was in separate file (json schema for example). I would also like to avoid some tags given by user - he should only pass me the values. Moreover, he should know where is the error, if he made some mistake.
Also, it would be nice if I could manipulate name of field in json (some clients may have different habits).
I didn't find anything which satisffied all of above requirements(entity reference and error handling are the most important). However - I just have heard about json schema, so maybe it provides such functionality (but I didn't find it though). Any helpful reference/example/lib? I will appreciate any help.
Just to be correct - imagine that the given json is a RELATIONAL database snapshot of the instance. I just want to create whole entity like the hibernate (or actually JPA) does :)
1. add jar of import org.json.JSONObject.
2. JSONObject object = new JSONObject(list)
2.1 object.has("teachers") if it is exists
2.2 JSONArray teacherArray = (JSONArray) object.get("teachers");
2.3 JSONObject teacherJsonObject = teacherArray .getJSONObject(0);
(if you have more than jsonobject in json arrary then itrate it.)
2.4 if(teacherJsonObject .has("id"))//you can check existence like this.
String id=teacherJsonObject .getString("id");
String payment=teacherJsonObject .getString("payment");
String name=teacherJsonObject .getString("name");
It may not be the best solution, but it's a working one.
Let's create a Parser class like the following:
public class Parser {
private List<Teacher> teachers;
private List<ClassRoom> classes;
public void parse() {
for (Teacher teacher : teachers) {
for (String classRoomId : teacher.getClasses()) {
for (ClassRoom classRoom : classes) {
if (classRoom.getId().equals(classRoomId)) {
teacher.getClassRooms().add(classRoom);
}
}
}
}
}
}
Modify your ClassRoom class to have a getter on the id field:
public class ClassRoom {
private String id;
private int room;
public String getId() {
return id;
}
}
And your Teacher class to get the Ids of classes AND the classRooms references:
public class Teacher {
private String id;
private double payment;
private String name;
private String[] classes;
private List<ClassRoom> classRooms = new ArrayList<>();
public String[] getClasses() {
return classes;
}
public List<ClassRoom> getClassRooms() {
return classRooms;
}
}
If you use the Gson library, you could then just parse your JSON like that:
Gson gson = new Gson();
Parser parser = gson.fromJson(jsonString, Parser.class);
parser.parse;
Now, every teacher will have their classRooms correctly referenced.