Swagger-UI generics mapping - java

is there anyway to correct Map method responses which use generics as response subfields with the appropriate implementation? Example:
Let's say I have a generic response class:
public class Response {
private String createdOn;
//other fields
private Map<String, Object> values;
private Model object;
//getters and setters
}
Generic model class:
public class Model{
}
Model Class A:
public class ModelA extends Model{
private String name;
private String lastname;
//getters and setters
}
And Model Class B:
public class ModelB extends Model{
private int number;
private boolean isTrue;
//getters and setters
}
And method doSomeStuff() which let's say returns a ResponseEntity< Response> where in this particular response "values" is a map< String, SomeRandomObject> and object is a ModelA object.
And suppose I have another method doSomeOtherStuff() which returns a ResponseEntity< Response> object where in that case "values" is a map< String, AnotherRandomObject> and object is a ModelB object.
Is there any way to map the response subfields of the first method to the appropriate ones? Using swagger-UI all I get is the following for both methods:
{
"createdOn": "string",
// other fields
"model": {},
"values": {}
}
Whereas I would like something like the following:
doSomeStuff():
{
"createdOn": "string",
// other fields
"model": {
"name": "string",
"lastname": "string",
}
"model": {
//All the subfields of SomeRandomObject
},
}
doSomeOtherStuff():
{
"createdOn": "string",
// other fields
"model": {
"number": "int",
"isTrue": "boolean",
}
"model": {
//All the subfields of AnotherRandomObject
},
}
Since it obviously cannot determine what type of objects are returned at the implementation level.
P.S. The above classes are as an example. I would really appreciate help to make the documentation work and not for ways to refactor it since the above logic is applied to more than 100 rest methods.
Edit: I am using springfox-swagger2 with springfox-swagger-ui.

Related

Java Json map object child as id

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

Json schema with anyOf fields to POJO

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

Is it possible to map Completion field with Context Suggester in spring-data-elasticsearch?

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"
}
]
}

Deserialize a varying number of objects into a list in Java using Jackson

My Spring Boot app makes a call to a REST API and receives a JSON with a varying number of entities. E.g.
{
"content": {
"guest_1": {
"name": {
"firstName": "a",
"lastName": "b"
},
"vip": false
},
"guest_2": {
"name": {
"firstName": "c",
"lastName": "d"
},
"vip": false
},
...more guests omitted...
}
}
There can be 1 to many guests and I don't know their number upfront. As you can see, they aren't in an array, they are objects instead.
I'd like to avoid deserializing into a class like
public class Content {
#JsonProperty("guest_1")
private Guest guest1;
#JsonProperty("guest_2")
private Guest guest2;
// More Guests here each having their own field
}
What I'd like to use is
public class Content {
private List<Guest> guests;
}
The #JsonAnySetter annotation I read about at https://www.baeldung.com/jackson-annotations looks promising but I couldn't get it to work.
3.2. Convert to an object at https://www.baeldung.com/jackson-json-node-tree-model looks also good but it didn't work out either.
I'm not sure if I can make Jackson do this in a declarative way or I should write a custom JsonDeserializer. Could you please help me?
#JsonAnySetter will work as it allows to specify a POJO type as second parameter. You could recreate the example JSON as, omitting setXXX() and getXXX() methods on POJOs for clarity:
private static class Content {
private Guests content;
}
private static class Guests {
private List<Guest> guests = new ArrayList<>();
#JsonAnySetter
private void addGuest(String name, Guest value) {
guests.add(value);
}
}
private static class Guest {
private Name name;
private boolean vip;
}
private static class Name {
private String firstName;
private String lastName;
}
With your JSON example will produce:
Content root = new ObjectMapper().readValue(json, Content.class);
root.getContent().getGuests().stream()
.map(Guest::getName)
.map(Name::getFirstName)
.forEach(System.out::println); // a, c

Stuck on parsing a JSON response to a Java Object in Android

I am making a query call to freebase and I receive a JSON response. The response has the following structure:
{
"code": "/api/status/ok",
"result": [
{
"/common/topic/image": [{
"guid": "#9202a8c04000641f8000000004b67f6d"
}],
"/people/person/profession": [{
"name": "Critic"
}],
"id": "/en/michael_jackson_1942",
"name": "Michael Jackson",
"type": "/people/person"
},
{
"/common/topic/image": [{
"guid": "#9202a8c04000641f800000001b90fdea"
}],
"/people/person/profession": [{
"name": "Actor"
}],
"id": "/en/michael_jackson_1970",
"name": "Michael Jackson",
"type": "/people/person"
}
],
"status": "200 OK",
"transaction_id": "cache;cache03.p01.sjc1:8101;2012-01-16T18:28:36Z;0055"
}
I need to parse this response in a ArrayList of java objects using GSON. To do this I need to create the class of the object with get/set and make it available to parse. Or is there another simpler way to do things ? I have used simple JSON strings by now, but in this case I can't remake the structure of the class I need. Basically in the end I need something like ArrayList<Person> where Person has all the attributes from the json string.
Any help is appreciated. Thank you.
The final solution, according with the answer below
public class FreebaseResponse {
#SerializedName("code")
public String code;
#SerializedName("result")
public ArrayList<Person> result;
#SerializedName("status")
public String status;
#SerializedName("transaction_id")
public String transaction_id;
}
public class Person {
#SerializedName("/common/topic/image")
public ArrayList<Person.Guid> imageGuid;
#SerializedName("/people/person/profession")
public ArrayList<Person.Profession> profession;
#SerializedName("id")
public String id;
#SerializedName("name")
public String name;
#SerializedName("type")
public String type;
private class Guid
{
#SerializedName("guid")
public String guid;
}
private class Profession
{
#SerializedName("name")
public String name;
}
}
I guess you can create a FreebaseResponse class that contains code, result (ArrayList<Person>), etc fields and use Gson to deserialize. the names that are not valid identifiers, e.g. /common/topic/image will be a problem. I haven't tried it myself but it seems to me that SerializedName annotation should do the trick.
If you need all the fields, the way you mentioned seems to me like the way to go. If you only want a small part of the data, you can get it directly. In general, I think that since JSON is meant for object representation, it is better to create the appropriate class. It will ease your way in the future as well, as you will need more from this data.

Categories