I have a Spring boot application where I am using RestTemplate to call a Rest API and I receive following JSON formatted response:
{
"data": [
{
"id": "1",
"type": "type1",
"config": {
"property1" : "value1",
"property2" : "value2"
}
},
{
"id": "2",
"type": "type2",
"config": {
"property3" : "value3",
"property4" : "value4",
"propArray": [ "element1", "element2"]
}
}
]
}
The individual elements within array 'data' has few different structures (2 examples above) where I would like to map different Class Types with individual elements which depends on the value of the element 'type'.
For example value 'type1' should map to an object of Class type 'Type1' and so on.
I have Classes created as below:
MyResponse:
public Class MyResponse {
List<Data> data;
..
\\getter and setters
}
Data:
public Interface Data {}
Type1:
public Class Type1 implements Data {
private String property1;
private String property2;
..
\\getter and setters
}
Type2:
public Class Type1 implements Data {
private String property3;
private String property4;
private List<String> propArray;
..
\\getter and setters
}
How can I map above conditional structure?
Only way I could think of it is get the returned value a String, convert it to JSONObject and process it to create your instances of your classes. For example,
String response = restTemplate.<your-uri>
JSONObject jsonObject = new JSONObject(response);
if (jsonObject.get(type).equals(type1) {
Type1 type1 = new Type1();
// set values
} else if (jsonObject.get(type).equals(type2) {
Type2 type2 = new Type2()
// set values
}
However, this is not scalable and if you are going add more and more types it would be very difficult to maintain a clean code.
Another way you can do this is to create a General Class and receive the response as List of that class. In this way Spring-boot/ jackson cand do the mapping. Again you have to add code to create the other classes from this general class. As Sam pointed out in the comment, this would be a preferred one since Jackson is faster than JSONObject. Here is sample class would look like,
class Response {
private Integer id;
private String type;
private Map<String, Object> config;
}
You still have to check the type and map to corresponding class.
Instead of writing such messy code, I would consider if I could re-architect your design/ response sent if you have control over it.
Related
I'm developing a series of clients to webservices and I'm observing that they follow a simple json structure, like this:
{
"typex": { "property1" : "value"},
"page": 1,
"count": 200,
"next_page": 2
}
I have near 15 webservices each returning the same structure, where the main information is within the object "typex" and their properties.
Today, these returns needs to have, at least, 2 classes, one for outer data and one for inner information.
public class TypeXWrapper {
#JsonProperty
TypeX typex;
#JsonProperty
Integer page;
#JsonProperty
Integer count;
#JsonProperty
Integer next_page;
}
public class TypeX {
#JsonProperty
String property1;
}
In this way, it would be necessary to create 30 classes.
Is there any way to implement some kind of generalization for situations like this?
I was thinking something like:
public class GenericWrapper<InnerClass> {
#JsonProperty
InnerClass data;
#JsonProperty
Integer page;
#JsonProperty
Integer count;
#JsonProperty
Integer next_page;
}
public class TypeX {
#JsonProperty
String property1;
}
But the problem is that the root property of the inner class data, changes for each webservice endpoint. That means that the next answer, for example TypeY, the property will have the name "typey", like:
{
"typey": { "property2" : "value"},
"page": 1,
"count": 200,
"next_page": 2
}
Is there anything that I could use to achieve this generalization? The environment and frameworks are Spring Boot 2.1.18 (can change if needed), using resttemplate with the return object encapsulated in ParameterizedTypeReference.
Thanks!
I need to consume a REST endpoint using spring RestTemplate. Below is the sample response from the endpoint and I need to fetch nested employee json and map it to model object. I found various complex solutions like
Deserializing response to a wrapper object and then fetching Employee object from it
Getting the response as a string and then converting it to json and deserializing
None of these solutions are simple and clean. Not sure if there is a clean and automatic way of doing it like this
ResponseEntity<Employee> response = restTemplate.exchange(URL,..?..);
Response -
{
"employee": {
"id": "123",
"first_name": "foo",
"last_name": "bar"
},
"session": {
"id": "1212121",
"createdDate": "2022-08-18T19:35:30Z"
}
}
Model object -
public class Employee {
private long emplId;
private String fName;
private String lName;
}
RestTemplate can do all of this for you, but first, your object doesn't match your response.
Ideally, you would have a Class that has an Employee and Session, like this
public class HaveSomeClassMan {
private Employee employee;
private Session session;
}
Then you can do
HaveSomeClassMan haveSomeClassMan = restTemplate.postForObject("URL", "requestObject", HaveSomeClassMan.class);
But, since you already have a JSON string, here's how you can convert it to a JSONObject, get the Employee out of it, and use the ObjectMapper to convert it to an Employee object.
JSONObject jsonObject = new JSONObject(s); // where s is your json string
String theEmployee = jsonObject.get("employee").toString(); // get the "employee" json from the JSONObject
Employee employee = new ObjectMapper().readValue(theEmployee, Employee.class); // Use the object mapper to convert it to an Employee
You do need to pay attention, though, as your model doesn't line up with your json. Your model calls the first name field, fname, yet your json has this as first_name. These properties all need to line up. If you want to keep your model as you've defined it, you need to annotate your fields with what their corresponding json field is, like this:
#JsonProperty("first_name")
private String fName;
This tells the mapper to map the JSON field first_name to the property fName. Regardless of whether you let RestTemplate do the mapping for you or you do it manually via the JSONObject approach, your model needs to match your JSON string. You can do this implicitly just by naming the fields in your model the same as they are in your JSON, or explicitly by adding the #JsonProperty to each field that doesn't implicitly map.
I'm developing a Spring Boot application with Spring Data JPA. I'm using a custom JPQL query to group by some field and get the count. Following is my repository method.
#Query("SELECT v.status.name, count(v) as cnt FROM Pet v GROUP BY v.status.name")
List<Object[]> countByStatus();
It's working and result is obtained as follows:
[
[
"pending",
1
],
[
"available",
4
]
]
However, I would like my Rest endpoint to respond with an output which is formatted like this
{
"pending": 1,
"available": 4
}
How can I achieve this?
Basically you want to produce a JSON where its properties ("pending", "available") are dynamic and come from the SELECT v.status.name part of the query.
Create a DTO to hold the row values:
package com.example.demo;
public class ResultDTO {
private final String key;
private final Long value;
public ResultDTO(String key, Long value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Long getValue() {
return value;
}
}
Change your query to create a new ResultDTO per row:
#Query("SELECT new com.example.demo.ResultDTO(v.status.name, count(v)) as cnt FROM Pet v GROUP BY v.status.name")
List<ResultDTO> countByStatus();
"com.example.demo" is my package, you should change it to yours.
Then from your service class or from your controller you have to convert the List<ResultDTO> to a Map<String, Long> holding all rows' keys and values.
final List<ResultDTO> repositoryResults = yourRepository.countByStatus();
final Map<String, Long> results = repositoryResults.stream().collect(Collectors.toMap(ResultDTO::getKey, ResultDTO::getValue));
Your controller should be able to transform final Map<String, Long> results to the desired JSON
I'm in the process of porting some microservices from SpringBoot1.5 to 2.1.
We are using spring-data-redis. it seems the default internal moves from jedis to lettuce.
The thing is we now observe some weird behaviours, when we save an object and then retrieve it, there is a tiny difference:
empty list attributes are replaced with null.
Here is an example:
//repo
public interface TestRepository extends CrudRepository<Test, String> {}
...
//object
#RedisHash(timeToLive = 60)
public static class Test{
#Id private String id;
int age;
List<String> friends;
}
...
//saving then retreiving
Test test = new Test("1", 15, Collections.emptyList());
System.out.println(test);
testRepository.save(test);
Test testGet = testRepository.findById("1").get();
System.out.println(testGet);
and here is what happens:
//before
{
"id": "1",
"age": 15,
"friends": []
}
//after
{
"id": "1",
"age": 15
}
the friends empty list has disappeared. This new behaviour affects our code in many places leading to NullPointerExceptions etc.
Apparently, there are multiple serializers available but this doesn't seem to have any effect. Any idea?
https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis:serializer
for reference:
springBootVersion = '2.1.5.RELEASE'
springCloudVersion = 'Greenwich.SR1'
I met this problem too. I solved it like this:
#RedisHash(timeToLive = 60)
public class MyData implements Serializable {
#Id
private String id;
private List<Object> objects = new ArrayList<>();
}
If i will save MyData with empty list objects, when i pull it from Redis, objects in it will not be null and will be empty list. If i will save 'MyData' with not empty objects, objects not will be lost after deserialization.
I would like to know how to access a deep collection class attribute in a GET request. My endpoint maps my query strings through #ModelAttribute annotation:
Given that:
public class MyEntity
{
Set<Item> items;
Integer status;
// getters setters
}
public class Item
{
String name;
// getters setters
}
And my GET request: localhost/entities/?status=0&items[0].name=Garry
Produces bellow behavior?
#RequestMapping(path = "/entities", method = RequestMethod.GET)
public List<MyEntity> findBy(#ModelAttribute MyEntity entity) {
// entity.getItems() is empty and an error is thrown: "Property referenced in indexed property path 'items[0]' is neither an array nor a List nor a Map."
}
Should my "items" be an array, List or Map? If so, thereĀ“s alternatives to keep using as Set?
Looks like there is some problem with the Set<Item>.
If you want to use Set for the items collection you have to initialize it and add some items:
e.g. like this:
public class MyEntity {
private Integer status;
private Set<Item> items;
public MyEntity() {
this.status = 0;
this.items = new HashSet<>();
this.items.add(new Item());
this.items.add(new Item());
}
//getters setters
}
but then you will be able to set only the values of this 2 items:
This will work: http://localhost:8081/map?status=1&items[0].name=asd&items[1].name=aaa
This will not work: http://localhost:8081/map?status=1&items[0].name=asd&items[1].name=aaa&items[2].name=aaa
it will say: Invalid property 'items[2]' of bean class MyEntity.
However if you switch to List:
public class MyEntity {
private Integer status;
private List<Item> items;
}
both urls map without the need to initialize anything and for various number of items.
note that I didn't use #ModelAttribute, just set the class as paramter
#GetMapping("map")//GetMapping is just a shortcut for RequestMapping
public MyEntity map(MyEntity myEntity) {
return myEntity;
}
Offtopic
Mapping a complex object in Get request sounds like a code smell to me.
Usually Get methods are used to get/read data and the url parameters are used to specify the values that should be used to filter the data that has to be read.
if you want to insert or update some data use POST, PATCH or PUT and put the complex object that you want to insert/update in the request body as JSON(you can map that in the Spring Controller with #RequestBody).