How to get the underlying String from a JsonParser (Jackson Json) - java

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
}

Related

How to parse JSON element that can be either simple string or an object into Class Object

I am not able to unmarshall a JSON key which can hold either a string value or an another JSON Object using Jackson Library.
Ex:- Below are the two possible values.
1)
"ProviderData": {
"INVALID": "HEX",
"#text": "Sample"
}
2)
"ProviderData": "1C"
Could someone please verify and suggest me on this issue.
You can write custom deserialiser and handle these both cases or write two constructors for ProviderData POJO class and properly use JsonCreator and JsonCreator annotations. See below example:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jsonFile, Response.class));
}
}
class Response {
#JsonProperty("ProviderData")
private ProviderData data;
// getters, setters, toString
}
class ProviderData {
private static final String INVALID_NAME = "INVALID";
private static final String TEXT_NAME = "#text";
#JsonProperty(INVALID_NAME)
private final String invalid;
#JsonProperty(TEXT_NAME)
private final String text;
#JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public ProviderData(String invalid) {
this(invalid, null);
}
#JsonCreator
public ProviderData(#JsonProperty(INVALID_NAME) String invalid, #JsonProperty(TEXT_NAME) String text) {
this.invalid = invalid;
this.text = text;
}
// getters, toString
}
For this JSON payload:
{
"ProviderData": {
"INVALID": "HEX",
"#text": "Sample"
}
}
Above example prints:
Response{data=ProviderData{invalid='HEX', text='Sample'}}
And for String primitive JSON payload:
{
"ProviderData": "1C"
}
Above example prints:
Response{data=ProviderData{invalid='1C', text='null'}}
As you can see, JSON Object is mapped properly using 2-arg constructor and String primitive is mapped using 1-arg constructor and we assume that this value means invalid key from JSON Object example.
See also:
Custom JSON Deserialization with Jackson.
sequentially deserialize using Jackson.
Deserialize strings and objects using jackson annotations in java.
you could deserialize to JsonNode and then extract the contents individually, or deserialize to an Object and use instanceof to determine if it's a Map or another type, or use a custom deserializer to unpack the data into a custom object that handles both cases.

Merge json payload values to Object

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

Is there any way to ignore JsonProcessingException without breaking deserialization process

I'm looking into a solution for Jackson deserialization JSON to an instance of a class without breaking the whole process, currently when I do something like:
If Actor.class was like:
#JsonInclude(JsonInclude.Include.NON_EMPTY)
#JsonPropertyOrder(alphabetic = true)
public abstract class BaseDTO {
}
public class Character extend BaseDTO {
private LocalDateTime updatedDate;
private String name;
// Setters and getters
}
and deserialize json {"updatedDate":"N/A", "name": "Jon Snow"} like:
String json = "{\"updatedDate\":\"N/A\", \"name\": \"Jon Snow\"}";
ObjectMapper mapper = new ObjectMapper();
final Character character = mapper.reader().forType(Character.class).readValue(json);
Or as Play Framework directly:
final Character character = Json.fromJson(json, Character.class);
I definitely will get an exception like:
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDateTime` from String "N/A": Text 'N/A' could not be parsed at index 0
And InvalidFormatException actually is a JsonProcessingException, there is also MismatchedInputException and other exceptions, so I need somehow gracefully continue with processing and get the object character and have name value at least instead of stopping it at all.
I prefer to:
Use annotations to config the parser or any solution to be applied to BaseDTO.
Logging the issue in the log file so I know that something wrong happened.
I really can't find the way right now without a huge effort, so I wonder if there is any out-of-box solution do that without re-invent the wheel.
I am assuming that you wannt ot consider string like N/A as null. Using a custom deserializer it can be achieved like this.
class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
#Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext context) throws IOException {
if ("N/A".equals(p.getText())) return null;
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(p.getText()));
}
}
Hope this helps!

java - Cannot get rid of '\' character in JSON once deployed by Lambda/Serverless

I am creating an API (written in Java) which I am deploying through serverless which ports to a AWS Lambda function. All aspects of the API function great except for the fact that the requests which are returned include the '\' character in front of all quotes.
To put this into perspective, I have a person class which contains instance variables for name (String) and mood (String). I then have my class which uses this class to get and create a Person object, and then Jackson is used to parse this into JSON format. This is what is returned to the handler function (for lambda) and is displayed as the "object body".
public class Person{
String name;
String mood;
//getters and setters and constructor
}
Then, later on there will be something in a different class like
Person person = new Person("bob", "good");
Which would be passed into my method which is supposed to convert things to JSON:
private String convStrToJson(Person person) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(person);
return json;
}
If I were to print this in the output, I'd get something like:
{"name":"bob","mood":"good"}
Which is what I want and expect. However, when deployed and called via GET request, the result is:
"{\"name\":\"bob\",\"mood\":\"good\"}"
I've tried several strategies, including additions to the parsing method such as:
json = json.replace("\"", "");
Which removes the quotes fully from both outputs, or:
json = json.replace("\\","");
Which has no effect at all. I also tried both of these as replaceAll methods and that just messed things up even more. I'm not sure what else I can do to get rid of these '\' characters, I understand why they're there but I don't know how to stop that. Any assistance is appreciated.
Okay so I figured it out. Turns out serverless not only includes Jackson, but actually in the layout it creates for handling responses, the "setObjectBody" section will accept any kind of object and use Jackson to parse it to JSON. This is where I messed up. I assumed it would only accept Strings, which is where the double encoding was occurring. Now, if I pass in the Person object, serverless/Jackson handles it appropriately for me and the expected output is returned. I'll include code snippets below to better demonstrate this solution. Serverless creates a 'handler' class which has a template including a method called handleRequest. Once filled in, this class now looks like this:
public class GetStatusHandler implements RequestHandler<Map<String, Object>, ApiGatewayResponse> {
private static final Logger LOG = Logger.getLogger(GetStatusHandler.class);
#SuppressWarnings("unchecked")
public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) {
BasicConfigurator.configure();
LOG.info("received: " + input);
try {
Map<String, String> pathParameters = (Map<String, String>) input.get("queryStringParameters");
if(pathParameters == null) {
LOG.info("Getting details for all persons ");
PersonControl control = new PersonControl();
Person[] result = control.myGetHandler(context);
return ApiGatewayResponse.builder()
.setStatusCode(200)
.setObjectBody(result)
.setHeaders(Collections.singletonMap("X-Powered-By", "AWS Lambda & serverless"))
.build();
}else {
String name = pathParameters.get("name");
LOG.info("Getting details for "+name);
PersonControl control = new PersonControl();
Person result = control.myGetHandler(name, context);
return ApiGatewayResponse.builder()
.setStatusCode(200)
.setObjectBody(result)
.setHeaders(Collections.singletonMap("X-Powered-By", "AWS Lambda & serverless"))
.build();
}
}catch(Exception e) {
LOG.error(e, e);
Response responseBody = new Response("Failure getting person", null);
return ApiGatewayResponse.builder()
.setStatusCode(500)
.setObjectBody(responseBody)
.setHeaders(Collections.singletonMap("X-Powered-By", "AWS Lambda & serverless"))
.build();
}
}
}
Not that when returning the ApiGatewayResponse (via builder), an object is simply passed in to the .setObjectBody method ('result') which serverless automatically converts to JSON for us. Thats it! No parsing to JSON necessary in the code.
The response can be a user defined object as below
class Handler implements RequestHandler<SQSEvent, CustomObject> {
public CustomObject handleRequest(SQSEvent event, Context context) {
return new CustomObject();
}
}
Sample code can be found here.
Just use the Google Gson java library that can be used to convert Java Objects into their JSON representation.
Gson gson = new Gson();
gson.toJson(person);

Treating "N/A" value as null with Jackson

I am getting data from an external JSON API and parsing the result with Jackson. Unfortunately, that API returns all fields as String and those are filled with "N/A" when data is unavailable.
I would like to replace those fields with null, especially as those are frustrating when I try to convert JSON String fields to more informative Java fields.
A custom DeserializationProblemHandler worked for Integer fields (see below), but it was useless for Java 8's LocalDate fields. Furthermore, it reacts to a problem rather than anticipating it.
I was unable to find a pre-processor to configure into my ObjectMapper and am uneasy with the idea of overriding the BeanDeserializer.
Do you know of a better/cleaner solution to handle this kind of situations? Thanks!
DeserializationProblemHandler
new DeserializationProblemHandler() {
#Override
public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException {
return "N/A".equals(valueToConvert) ? null : super.handleWeirdStringValue(ctxt, targetType, valueToConvert, failureMsg);
}
}
Error message when processing "N/A" in LocalDate field
Can not deserialize value of type java.time.LocalDate from String "N/A": Text 'N/A' could not be parsed at index 0
(works fine when there is date in the data)
I feel like there ought to be a better way of doing this, but the following is the only solution I was able to come up with.
Create a new JsonDeserializer that handles "N/A" input. The following example handles strings:
public class EmptyStringDeserializer extends StdScalarDeserializer<String> {
public EmptyStringDeserializer() {
super(String.class);
}
#Override
public String deserialize(JsonParser parser, DeserializationContext ctx) throws IOException {
final String val = parser.getValueAsString();
if ("N/A".equalsIgnoreCase(val))
return null;
return val;
}
}
The class is registered with an ObjectMapper like this:
SimpleModule simpleModule = new SimpleModule().addDeserializer(String.class, new EmptyStringDeserializer());
ObjectMapper om = new ObjectMapper().registerModule(simpleModule);
You'll probably want to collect all your converters in a module named for the API that is making you handle things this way.

Categories

Resources