I want to merge the REST PATH payload to the 'Entity' object after getting it from a database, so that only the attributes provided in payload will be updated in entity. Hence, I want to ensure that only the attributes provided as part of patch payload will be updated safely.
I am using Spring Rest Controller with Hibernate entities.
#PatchMapping(value = "/{id}")
public Resource<DepartmentPEO> update(#PathVariable Long id,
#RequestBody JSONObject payload) throws Exception
{
DepartmentPEO eo = departmentService.getRow(id);
// Have to do something to update the eo object from jsonObject.
// Some api to update eo
eo = departmentService.update(id, eo);
Resource<DepartmentPEO> resource = new Resource<>(eo);
DepartmentPEO dept = resource.getContent();
id = dept.getDeptSeq();
resource.add(
linkTo(methodOn(DepartmentsRestController.class).getRow(id))
.withSelfRel());
return resource;
}
Only the modified attributes will be sent as part of payload to server instead of sending all attributes.Resource(entity) will have nested list of objects (One-to-many). Am looking for the pool-proof solution for this use case and also believe this is common/basic for every rest api supported apps.
Pointing to any API to solve this would greatly appreciated!
Thank you
Here is a working example using Jackson's ObjectMapper and BeanUtils from Spring (since I assume you're using Spring) :
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.springframework.beans.BeanUtils;
public class StackOverflow {
#Test
public void mergeTest() throws IOException, JSONException {
DepartmentPEO existingDepartement = existingDepartmentPEO();
JSONObject payload = new JSONObject();
payload.put("name", "newName");
DepartmentPEO result = mergeToObject(payload, existingDepartement);
assert result.getName().equals("newName");
assert result.getId().equals("1");
}
private DepartmentPEO existingDepartmentPEO() {
DepartmentPEO existingDepartement = new DepartmentPEO();
existingDepartement.setId("1");
existingDepartement.setName("oldName");
return existingDepartement;
}
private DepartmentPEO mergeToObject(JSONObject payload, DepartmentPEO object) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
DepartmentPEO updateRequest = objectMapper.readValue(payload.toString(), DepartmentPEO.class);
BeanUtils.copyProperties(updateRequest, object, "id");
return object;
}
}
Here, I transform the JSONObject into a DepartmentPEO class then I copy this object into the existing one ignoring the field id.
You may want to have a generic way to ignore null fields from the JSONObject, then you can refer to this post for instance How to ignore null values using springframework BeanUtils copyProperties?
I would advice to send directly the DepartmentPEO object into the REST method signature instead of using a JSONObject.
Regards
Related
I am trying to invoke a third-party API through REST call in Spring. Currently, I'm using postForObject. I am converting the request class to string and calling the post for object. The response is taken as string and then converted it into the class. I have defined the class with below parameters
Class responseDto {
private Arraylist < Response > response;
getResponse();
setResponse();
}
Response {
String code;
String trid;
Getters();
Setters();
}
I am using Jackson dependency to serialize and deserialize. This class is working fine for the below response:
{
"response":[
{
"code":"100",
"trid":"123"
}
]
}
However, in error scenario, the request returns a JSON class with the same name 'response' as given below
{
"response":{
"code":"700",
"trid":"123"
}
}
The deserialize fails for the class I defined with some JSON mapping exception:
com.fasterxml.jackson.databind.JsonMappingException: Can not
deserialize instance of java.util.ArrayList out of START_OBJECT token
How can I resolve this issue in Java and Spring?
SOLUTION 1: Using #JsonFormat ( > 2.6 version)
Just annotate your field with #JsonFormat as
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
public class ResponseDto {
#JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<Response> response;
public List<Response> getResponse() {
return response;
}
public void setResponse(List<Response> response) {
this.response = response;
}
}
SOLUTION 2: Setting DeserializationFeature
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
// global setting, can be overridden using #JsonFormat in beans
// when using #JsonFormat on fields, then this is not needed
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
ResponseDto dto = mapper.readValue(stringResponse, ResponseDto.class);
}
Now response node in json containing single object, single object array, multiple object array will be successfully parsed as list of Response object.
I am a begineer in java development but has previous experience on programming languages like PHP and Python. So little confused on how to proceed on spring boot with the development.
I am developing a rest API which has the following request
{
"key":"value",
"key1":"value1",
"platform_settings":[
{"key":"value"}
]
}
What I did
I created a RestController which accepts the http request and created a function for the resource
public Share share(#RequestBody final Share share) {
LOGGER.debug("This is the request", share);
return share; //
}
Question 1 : If it was any other programming language like PHP or Python, there will be helper function which will accept the json request and convert it to object which I can easily work on.
In python it is as simple as
import json
import requests
response = requests.get(...)
json_data = json.loads(response.text)
//can work on json_data anyway I want.
But in java, I will have to create a POJO class, or have jackson/JPA entity as dependency which will map the request to a Class (Which I should predefine with the requests).
Is there any better way I can do this? For every request I make, I will have to create a Class which the request can be mapped to and I will have to define the class
Entity
package com.payunow.socialsharemodule.models;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
#Entity
public class Share {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String key;
private String key1;
private Map<String,String> platform_settings;
public Share(String name, String description,Map<String,String> platform_settings) {
this.key = key;
this.key1 = key1;
this.platform_settings = platform_settings;
}
//for JPA
public Share() {}
public String getKey() {
return key;
}
public String getKey1() {
return key1;
}
public Map<String,String> getPlatform_settings() {
return platform_settings;
}
}
For every request I make, I will have to create a class defining all its variables inside. Is this the only way to do this?
You need to have Jackson dependecy for coversion of json to java object. But spring provides it by default, so you don't have to add it explicitly.
You don't need a JPA Entity. This is needed only when you want to store the recieved data into database.
Just to recieve the request you don't have to create a separate pojo class. Look at this code
#PostMapping("/json")
public JSONObject getGeneric(#RequestBody String stringToParse){
JSONParser parser = new JSONParser();
JSONObject json = null;
try {
json = (JSONObject) parser.parse(stringToParse);
} catch (ParseException e) {
e.printStackTrace();
}
return json;
}
As you can see here it takes a string as a request and converts it into a generic JSONObject. So basically you can pass any json to this endpoint.
You CanUse ObjectMapper class it has methods like convertValue and realValue..
I have JUnit test of a method which send a JAX-RS POST call.
To be independent from external resources I have mocked the REST client and said that a dummy response should be returned. Works great, no problem. But:
When calling myResponse.readEntity(String.class) I always get the following Exception:
java.lang.IllegalStateException: RESTEASY003290: Entity is not backed by an input stream
Here is my code snippet which fails:
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.Test;
public class SimpleTest {
#Test
public void testReadResponse() {
final JsonObject responseContent = new JsonObject();
responseContent.add("field", new JsonPrimitive("This is a JSON for testing."));
final String expected = responseContent.toString();
final Response.ResponseBuilder builder = Response.ok()
.entity(responseContent.toString())
.header("Content-Type", MediaType.APPLICATION_JSON);
final Response dummyResponse = builder.build();
final String result = dummyResponse.readEntity(String.class); // <-- Exception is thrown here!
assertThat("JSON Strings are not identical.", result, is(expected));
}
}
and the Stacktrace:
java.lang.IllegalStateException: RESTEASY003290: Entity is not backed by an input stream
at org.jboss.resteasy.specimpl.BuiltResponse.readEntity(BuiltResponse.java:230)
at org.jboss.resteasy.specimpl.BuiltResponse.readEntity(BuiltResponse.java:219)
at de.me.myproject.SimpleTest.testReadResponse(SimpleTest.java:43)
In my production code, which calls a not mocked REST API, it returns a automatically build response, where the .readEntity(String.class) method works fine.
Response is an abstract class and RESTEasy has different sub classes for client and server, see BuiltResponse and ClientResponse. Not all methods are supported in each sub class.
Response#readEntity needs to be backed by an input stream:
Method throws an ProcessingException if the content of the message cannot be mapped to an entity of the requested type and IllegalStateException in case the entity is not backed by an input stream or if the original entity input stream has already been consumed without buffering the entity data prior consuming.
A BuiltResponse is never backed by an input stream and therefore you get a IllegalStateException.
You can use Response#getEntity, it doesn't need an input stream.
Thanks for the hint.
I ended up with the following that worked for me. Simply return a new instance of the class below.
class MockResponse extends BuiltResponse {
private Object entity;
public MockResponse() {
}
public MockResponse(Object entity) {
this.entity = entity;
}
#Override
public <T> T readEntity(Class<T> type) {
return (T) entity;
}
#Override
public <T> T readEntity(Class<T> type, Type genericType, Annotation[] anns) {
return (T) entity;
}
}
Today I went through the same situation. Finally I have the below solution. Just mock the readEntity method in BuiltResponse to return whatever response you need. It worked for me.
I am developing a web application with AngularJS and WildFly using Spring also.
My problem is that I am going nuts because the annotation #requestBody appears to work wrong.
This is my service:
#ResponseBody
#RequestMapping(value = "/keyuser", method = RequestMethod.POST,
consumes = "application/json")
public KeyProfileUserSummary updateEmployee(#RequestBody KeyProfileUserSummary keyUser) {
return null;
}
And this is are the members of my object KeyProfileUserSummary:
private Integer id;
private String login;
private String password;
private String firstname;
private String lastname;
private UserRole userRole;
I don't know what is going on but I have tested this service with other types of objects and it works perfectly, but when defining KeyProfileUserSummary it is not working, I get ERROR 400 BAD REQUEST. I have tested to set the #RequestBody to "Object" so at least I can see what is coming, and from my front end, I am getting the following:
{id=3, login=aa, password=a, firstname=Martin, lastname=Müller, userRole=ROLE_USER}
UserRole is an Enum. Important to clearify that KeyProfileUserSummary is just a summary version of KeyProfileUser, but due to all the linked elements I get on the response, I decided to send this lighter class. Testing with KeyProfileUser worked perfectly, I get the JSON object on the Angular side and can send it back.
On the Angular side, I am not doing anything with the object. Just receive it on a list, and when pressing an edit button just send the element on the list back. This is the way I am sending it:
res = $http.post("url.../keyuser", user);
The thing is that I had everything working perfectly with KeyProfileUser, but as the database can get really huge and the reference are quite a lot, I decided to switch to this lighter class, but now I only get this ERROR 400 BAD REQUEST... And I am about to hang myself :P
Thanks for your help!
Ok so finally I found the solution.
In my KeyProfileUserSummary I only had one constructor that was taking a KeyProfileUser and set the attributes to the summary version:
public KeyProfileUserSummary(KeyProfileUser keyProfileUser) {
this.id = keyProfileUser.getId();
this.login = keyProfileUser.getLogin();
this.password = keyProfileUser.getPassword();
this.firstname = keyProfileUser.getPerson().getFirstname();
this.lastname = keyProfileUser.getPerson().getLastname();
this.userRole = keyProfileUser.getUserRole();
}
And apparently, setting a breakpoint in line 993 of the dispatchler servlet (thanks to #Clemens Eberwein for the tip) I realised that when parsing from a JSON object, the Jackson parser needs an empty constructor! So adding it solved it and works perfectly.
Note: for KeyProfileUser, it was working perfectly as we had the annotation #Entity for hibernate, and therefore the empty constructor was automatically created.
Try this out.. might be useful for you..
$http({
method: 'POST',
url: 'http://localhost:8080/keyuser',
data: user,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}}).then(function(result) {
console.log(result);
}, function(error) {
console.log(error);
});
If I were to guess, jackson is failing in deserializing/serializing your object. Here is an util I made:
import java.io.IOException;
import java.nio.charset.Charset;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class SerializeDeserializeUtil {
public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(
MediaType.APPLICATION_JSON.getType(),
MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
public static byte[] convertObjectToJsonBytes(Object object)
throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL);
return mapper.writeValueAsBytes(object);
}
public static <T> T deserializeObject(String jsonRepresentation,
Class<T> clazz) throws JsonParseException, JsonMappingException,
IOException {
ObjectMapper mapper = new ObjectMapper();
Object obj = mapper.readValue(jsonRepresentation.getBytes(), clazz);
return clazz.cast(obj);
}
#SuppressWarnings({ "unchecked", "rawtypes" })
public static byte[] convertObjectToJsonBytesWithCustomSerializer(
Object object, JsonSerializer serializer, Class clazz)
throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule sm = new SimpleModule();
sm.addSerializer(clazz, serializer);
mapper.registerModule(sm);
mapper.setSerializationInclusion(Include.NON_NULL);
return mapper.writeValueAsBytes(object);
}
}
Try creating a test just to serialize and deserialize the objec. create a KeyProfileUserSummary object and try deserializing/serializing too see if jackson complains.
A more easier way is to enable DEBUG logging and checking the log file, by default you don't get to see this kind of errors
Hope it helps.
If you add DEBUG logging for "org.springframework.web" org.springframework.web.servlet.DispatcherServlet should give you detailed information what causes the "400 Bad Request" error.
Details about Wildfly logging configuration can be found here
Based on your KeyProfileUserSummary class i guess the problem is the UserRole object which ist just userRole=ROLE_USER in the example above. As it is an object it should be enclosd by curly braces and the property name must be set. e.g. something like
userRole = { name = "ROLE_USER"}
If it is an enum, this answer might be helpful
Looking through the documentation and source code I don't see a clear way to do this. Curious if I'm missing something.
Say I receive an InputStream from a server response. I create a JsonParser from this InputStream. It is expected that the server response is text containing valid JSON, such as:
{"iamValidJson":"yay"}
However, if the response ends up being invalid JSON or not JSON at all such as:
Some text that is not JSON
the JsonParser will eventually throw an exception. In this case, I would like to be able to extract the underlying invalid text "Some text that is not JSON" out of the JsonParser so it can be used for another purpose.
I cannot pull it out of the InputStream because it doesn't support resetting and the creation of the JsonParser consumes it.
Is there a way to do this?
If you have the JsonParser then you can use jsonParser.readValueAsTree().toString().
However, this likely requires that the JSON being parsed is indeed valid JSON.
I had a situation where I was using a custom deserializer, but I wanted the default deserializer to do most of the work, and then using the SAME json do some additional custom work. However, after the default deserializer does its work, the JsonParser object current location was beyond the json text I needed. So I had the same problem as you: how to get access to the underlying json string.
You can use JsonParser.getCurrentLocation.getSourceRef() to get access to the underlying json source. Use JsonParser.getCurrentLocation().getCharOffset() to find the current location in the json source.
Here's the solution I used:
public class WalkStepDeserializer extends StdDeserializer<WalkStep> implements
ResolvableDeserializer {
// constructor, logger, and ResolvableDeserializer methods not shown
#Override
public MyObj deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
MyObj myObj = null;
JsonLocation startLocation = jp.getCurrentLocation();
long charOffsetStart = startLocation.getCharOffset();
try {
myObj = (MyObj) defaultDeserializer.deserialize(jp, ctxt);
} catch (UnrecognizedPropertyException e) {
logger.info(e.getMessage());
}
JsonLocation endLocation = jp.getCurrentLocation();
long charOffsetEnd = endLocation.getCharOffset();
String jsonSubString = endLocation.getSourceRef().toString().substring((int)charOffsetStart - 1, (int)charOffsetEnd);
logger.info(strWalkStep);
// Special logic - use JsonLocation.getSourceRef() to get and use the entire Json
// string for further processing
return myObj;
}
}
And info about using a default deserializer in a custom deserializer is at How do I call the default deserializer from a custom deserializer in Jackson
5 year late, but this was my solution:
I converted the jsonParser to string
String requestString = jsonParser.readValueAsTree().toString();
Then I converted that string into a JsonParser
JsonFactory factory = new JsonFactory();
JsonParser parser = factory.createParser(requestString);
Then I iterated through my parser
ObjectMapper objectMapper = new ObjectMapper();
while(!parser.isClosed()){
JsonToken jsonToken = parser.nextToken();
if(JsonToken.FIELD_NAME.equals(jsonToken)){
String currentName = parser.getCurrentName();
parser.nextToken();
switch (currentName) {
case "someObject":
Object someObject = objectMapper.readValue(parser, Object.class)
//validate someObject
break;
}
}
I needed to save the original json string for logging purposes, which is why I did this in the first place. Was a headache to find out, but finally did it and I hope i'm helping someone out :)
Building my own Deserialiser in which I wanted to deserialise a specific field as text i.s.o. a proper DTO, this is the solution I came up with.
I wrote my own JsonToStringDeserializer like this:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringEscapeUtils;
import java.io.IOException;
/**
* Deserialiser to deserialise any Json content to a String.
*/
#NoArgsConstructor
public class JsonToStringDeserializer extends JsonDeserializer<String> {
/**
* Deserialise a Json attribute that is a fully fledged Json object, into a {#link String}.
* #param jsonParser Parsed used for reading JSON content
* #param context Context that can be used to access information about this deserialization activity.
* #return The deserialized value as a {#link String}.
* #throws IOException
*/
#Override
public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
final TreeNode node = jsonParser.getCodec().readTree(jsonParser);
final String unescapedString = StringEscapeUtils.unescapeJava(node.toString());
return unescapedString.substring(1, unescapedString.length()-1);
}
}
Annotate the field you want to deserialize like this:
#JsonDeserialize(using = JsonToStringDeserializer.class)
I initially followed advice that said to use a TreeNode like this:
final TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
return treeNode.toString();
But then you get a Json String that contains escape characters.
What you are trying to do is outside the scope of Jackson (and most, if not all other Java JSON libraries out there). What you want to do is fully consume the input stream into a string, then attempt to convert that string to a JSON object using Jackson. If the conversion fails then do something with the intermediate string, else proceed normally. Here's an example, which utilizes the excellent Apache Commons IO library, for convenience:
final InputStream stream ; // Your stream here
final String json = IOUtils.toString(stream);
try {
final JsonNode node = new ObjectMapper().readTree(json);
// Do something with JSON object here
} catch(final JsonProcessingException jpe) {
// Do something with intermediate string here
}