Custom Deserialization using Jackson in Java - java

I am trying to implement a university project where I try to fetch values from two Json fields and map it to one pojo class.
Sample Json:
"event":[{"D17-32":0,"S10":"D"}]
Pojo class
public class Event {
#JsonDeserialize(using = SignalCustomDeserializer.class)
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
#JsonProperty("S10")
private Signal S10;
#JsonProperty("D17-32")
private String D17_32;
//Getter and setter implementation
}
Class which I need to serialize the fields to
public class Signal{
private String value;
private String detectorId;
private int detectorValue; //this value has to be fetched from another json
//Getter and setter implementation
}
Custom deserializer class
public class SignalCustomDeserializer extends JsonDeserializer {
#Override
public Signal deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String signalId = jsonParser.getCurrentName();
String signalVal = jsonParser.getValueAsString();
String detectorVal = jsonParser.getValueAsString("D01-16");
Signal signal = new Signal();
signal.setValue(signalVal);
signal.setDetectorId(getDetectorId(signalId));
return signal;
}
}
I am able to get the signalId and signalValue but I am unable to get the value for the other field. I am unsure if its available in the JsonObject when the custom deserialization class is called.

Related

Jackson gives StackOverflowError reading value in custom StdDeserializer

Having 2 simple classes like:
#Setter
#Getter
public class Continent {
private String id;
private String code;
private String name;
}
#Setter
#Getter
public class Country {
private String id;
private String alpha2Code;
private String alpha3Code;
private String name;
private Continent continent;
}
when reading the following yaml:
id: brazil
alpha2_code: BR
alpha3_code: BRA
name: Brazil
continent_id: south-america
I would like to use the continent_id to retrieve the Continent from a application scoped List<Continent>.
The best thing I could think of is using a custom Deserializer like:
public class CountryDeserializer extends StdDeserializer<Country> {
public CountryDeserializer() {
super(Country.class);
}
#Override
public Country deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// This works... the `continentId` is retrieved!
JsonNode node = jp.getCodec().readTree(jp);
String continentId = node.get("continent_id").asText();
// How to access application scoped continents? Use injectable value?
Continent continent = getContinent(continentId);
// Read value for other properties; don't want to read other properties manually!
Country country = jp.getCodec().readValue(jp, Country.class);
// But unfortunately this throws a StackOverflow...
country.setContinent(continent);
return country;
}
}
But the problem is I would like Jackson to automatically read the other properties.
I don't want to this manually as if in the future a property is added it might be forgotten, and with other entities with 20 properties this becomes very cumbersome...
I tried with Country country = jp.getCodec().readValue(jp, Country.class); but that gives stack overflow exception as it gets in a loop with the custom deserializer obviously.
Is there a way to solve this using Jackson, or is there another better approach to get and set the Continent in this scenario?
Note I'm working with a pre-defined set of domain classes I cannot change.
I can modify the object mapper and add mixins if needed.
Instead of using a CountryDeserializer I've implemented it using a ContinentReferenceDeserializer.
This way the other Country properties are deserialized "automatically".
It looks like:
public class ContinentReferenceDeserializer extends StdDeserializer<Continent> {
public ContinentReferenceDeserializer() {
super(Continent.class);
}
#Override
public Continent deserialize(JsonParser parser, DeserializationContext context) throws IOException {
String id = parser.getText(); // returns the continent id (`continent_id` in json)
Map<String, Continent> continents = (Map<String, Continent>) context.findInjectableValue("continents", null, null);
return continents.gett(id);
}
}
and it is used in the CountryMixIn like:
public abstract class CountryMixIn {
#JsonProperty("continent_id")
#JsonDeserialize(using = ContinentReferenceDeserializer.class)
abstract Continent getContinent();
}
Note that if you don't use Mix-ins but directly annotate domain/dtoa classes, above can be applied to these as well instead.
The ObjectMapper can be setup then like:
Map<String, Continent> continents = .. // get the continents
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Country.class, CountryMixIn.class);
mapper.setInjectableValues(new InjectableValues.Std().addValue("continents", continents));
and then can be called like:
String json = .. // get the json
Country country = mapper.readValue(json, Country.class);

Automatic deserialization of String to Object with Jackson

Context
Say you have:
public class Dto {
private String name;
private String List<Custom> customs;
// getters and setters...
}
and
public class Custom {
private String something;
private String else;
// getters and setters...
}
Your Spring MVC RestController receives a list of Dto:
#PostMapping
public String create(#RequestBody #Valid List<Dto> dtos) {
return myService.process(features);
}
Input
However, you know that the client-side service which will send data to your controller will send something like this:
[
{
"name": "Bob",
"customs": [
"{\n \"something\": \"yes\",\n \"else\": \"no\"\n }"
]
}
]
Notice how the List<Custom> actually ends up being received as a List<String>. Please assume this cannot be changed on the client-side and we have to deal with it on the server-side.
Question
Is there a Jackson annotation which would automagically take the input String and try to serialize it into a Custom class?
Attempts
A few things that didn't work, including:
#JsonSerialize(using = ToStringSerializer.class)
private List<Custom> customs;
along with
public Custom(String json) {
try {
new ObjectMapper().readerFor(Custom.class).readValue(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
As it is, we have had to change the customs type to List<String> and add a utility method which converts a String into a Custom using an ObjectMapper. This is rather dissatisfying.
You need to implement custom deserialiser or converter which would be used to convert given payload to required type. One trick, you could use is to create new ObjectMapper and use it for internal deserialisation.
Example usage:
class CustomConverter extends StdConverter<String, Custom> {
private final ObjectMapper mapper = new ObjectMapper();
#Override
public Custom convert(String value) {
try {
return mapper.readValue(value, Custom.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(value);
}
}
}
class Dto {
private String name;
#JsonDeserialize(contentConverter = CustomConverter.class)
private List<Custom> customs;
}
You need to create a custom Deserializer.
public class CustomDeserializer extends StdDeserializer<Custom> {
public CustomDeserializer() {
this(null);
}
public CustomDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Custom deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
int id = (Integer) ((IntNode) node.get("id")).numberValue();
String name = node.get("name").asText();
...
return new Custom(id, name, ...);
}
}
and register the deserializer on the Custom class:
#JsonDeserialize(using = CustomDeserializer.class)
public class Custom {
...
}

Can we use deserializer for getmapping in spring boot which has pojo similar to #beanparam of jersey

I have a spring boot app where I am planning to write an API which is a GET request but has many request parameters in it.
So I have created a POJO which maps the request params to the POJO. I wanted to deserialize it before mapping the request params to POJO.
How can I achieve that?
I have created the custom deserializer and annotated the POJO with #JsonDeserialize(using = "CustomDeserializer.class").
Call is not landing to the deserializer but mapping the fields directly to the POJO.
//pojo
#Getter
#Setter
#JsonDeserialize(using = CustomDeserializer.class)
public class GetRequest implements Serializable {
private int id;
private String date;
private List<Test> List;
private int uiRank;
private int fr;
private String city;
private String state;
private String country;
}
//Deserializer
#JsonComponent
public class CustomDeserializer extends JsonDeserializer<GetRequest> {
#Override
public GetRequest deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
//logic to deserialize
}
}
//controller
#RestController
public class TestController {
#GetMapping("/details")
public void getTestDetails(GetRequest getRequest) {
//logic
}
}
Cannot use queryparam annotation on an API in a controller as I have many query params. I need to use a POJO which is mapped with all the query params but before that deserializer needs to be executed.

Jackson deserialization error: no String-argument constructor/factory method to deserialize from String value

I am trying to deserialize the following JSON
{
"deliverLumpSum": 0.0,
"faxQId": "{\"type\":\"TAKEAWAY\",\"data\":{\"orderId\":\"AWSWD-AWSAW\",\"orderKey\":\"DERS34S32SD\"}}"
}
With help of the following custom deserializer
public class OrderIdDeserializer extends JsonDeserializer<OrderId> {
#Override
public OrderId deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
OrderId orderId = jsonParser.readValueAs(OrderId.class);
return orderId;
}
}
into the following object-structure
#Data
public class AddInfo {
protected double deliverLumpSum;
#JsonDeserialize( using = OrderIdDeserializer.class)
public OrderId orderId;
}
#Data
public class OrderId {
private String type;
private TakeawayData data;
}
#Data
public class TakeawayData {
private String orderId;
private String orderKey;
}
I get the following error
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot
construct instance of OrderId (although at least one Creator
exists): no String-argument constructor/factory method to deserialize
from String value
('{"type":"TAKEAWAY","data":{"orderId":"AWSWD-AWSAW","orderKey":"DERS34S32SD"}}')
What am I doing wrong and how can I solve this problem?
First, your JSON example ("faxQId":) doesn't match
your Java class AddInfo:
#JsonDeserialize( using = OrderIdDeserializer.class)
public OrderId orderId;
I guess this is just a copy-and-paste error and you really mean
#JsonDeserialize( using = OrderIdDeserializer.class)
public OrderId faxQId;
Now for the real problem.
Your JSON content after "faxQId": has a JSON string
containing JSON code (with properly escaped " quotes)
"faxQId": "{\"type\":\"TAKEAWAY\",\"data\":{\"orderId\":\"AWSWD-AWSAW\",\"orderKey\":\"DERS34S32SD\"}}"
instead of just a normal JSON object like
"faxQId": {"type":"TAKEAWAY","data":{"orderId":"AWSWD-AWSAW","orderKey":"DERS34S32SD"}}"
Therefore, in your deserializer you need an additional step
for deserializing this String to a Java object.
public class OrderIdDeserializer extends JsonDeserializer<OrderId> {
#Override
public OrderId deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
// Get the JSON text (with the \ already decoded):
// {"type":"TAKEAWAY","data":{"orderId":"AWSWD-AWSAW","orderKey":"DERS34S32SD"}}
String s = jsonParser.getValueAsString();
// We need a second parser for deserializing the JSON text
JsonParser jsonParser2 = jsonParser.getCodec().getFactory().createParser(s);
// Convert the JSON text into an object
return jsonParser2.readValueAs(OrderId.class);
}
}

Jackson - Deserialize empty String Member to null

I like to deserialize with Jackson an empty String member ("") to null. The Deserialization Feature "ACCEPT_EMPTY_STRING_AS_NULL_OBJECT" can for this unfortunately not be used (see link).
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(Include.NON_NULL)
public class Supplier {
private Integer id;
private String name;
private String image;
private String link;
private String description;
}
So after deserialization of the following JSON String the string members "link" and "image" should be null and not "".
{"id":37,"name":"Life","image":"","link":"","description":null}
I am looking for a way to write an own deserializer which can be used for String members of a POJO. Is there a way to achieve this? I am using faster Jackson 2.6.0.
The custom deserializer can be done as follows in Jackson 2.6.0.
public class SupplierDeserializer extends JsonDeserializer<Supplier> {
#Override
public Supplier deserialize(JsonParser jp, DeserializationContext context) throws IOException, JsonProcessingException {
Supplier sup = new Supplier();
JsonNode node = jp.readValueAsTree();
sup.setId(node.get("id").asInt());
sup.setDescription(node.get("description").asText());
String image = node.get("image").asText();
if("".equals(image)) {
image = null;
}
sup.setImage(image);
String link = node.get("link").asText();
if("".equals(link)) {
link = null;
}
sup.setLink(link);
sup.setName(node.get("name").asText());
return sup;
}
}
Register the custom deserialiser with the Supplier class
#JsonDeserialize(using = SupplierDeserializer.class)
public class Supplier {
private Integer id;
private String name;
private String image;
private String link;
private String description;
// getters and setters
}
Call the ObjectMapper class to parse the JSON data
String jsonData = "{\"id\":37,\"name\":\"Life\",\"image\":\"\",\"link\":\"\",\"description\":null}";
Supplier sup = new ObjectMapper().readValue(jsonData, Supplier.class);

Categories