Ignore RootNode and custom mapping in Jackson/spring/Java - java

How i can ignore rootname if I dont need it? I need only bill.
if I remove "version" from json work fine..
my error on console log
2019-07-27 19:20:14.874 WARN 12516 --- [p-nio-80-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unexpected token (FIELD_NAME), expected END_OBJECT: Current token not END_OBJECT (to match wrapper object with root name 'bill'), but FIELD_NAME; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (FIELD_NAME), expected END_OBJECT: Current token not END_OBJECT (to match wrapper object with root name 'bill'), but FIELD_NAME
at [Source: (PushbackInputStream); line: 8, column: 2]]
my json look like this
{
"bill":
{
"siteId":"gkfhuj-00",
"billId":"d6334954-d1c2-4b51-bb10-11953d9511ea"
},
"version":"1"
}
my class for json
i try use JsonIgnoreProperties but its dont help also i write "version"
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonRootName(value = "bill")
public class Bill {
private String siteId;
private String billId;
//getters and setters
my post method lisen object Bill
#PostMapping("/bill")
#ResponseBody
public ResponseEntity<String> getBill(#RequestBody Bill bill)

As you are relying on the Spring boot through annotations and Jackson, custom deserializer will work flawless in here. You have to create the deserializer class as show below
public class BillDeserializer extends StdDeserializer<Bill> {
public BillDeserializer() {
this(null);
}
public BillDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Bill deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode billNode = jp.getCodec().readTree(jp);
Bill bill = new Bill();
bill.setSiteId(billNode.get("bill").get("siteId").textValue());
bill.setBillId(billNode.get("bill").get("billId").textValue());
return bill;
}
}
Now you have to instruct your Jackson to use this deserializer instead of the default one for the Bill class. This is done by registering the desearilizer. It can be done through a simple annotation on the Bill class like #JsonDeserialize(using = BillDeserializer.class)
Your Bill class will typically look as specified below
#JsonDeserialize(using = BillDeserializer.class)
public class Bill {
private String siteId;
private String billId;
//getters and setters
}

Related

Accept empty String for Enum in REST calls in Spring Boot application

I made a Spring Boot 2 REST application. I'm consuming REST with Angular. I've a problem with enumeration.
A typical enum server side is:
public enum EngineType {
DIESEL, METHANE, ELECTRIC;
#Nullable
public static EngineType valueOfNullable(String value) {
try {
return valueOf(value);
} catch (Exception e) {
return null;
}
}
}
Some entities use these enum as fields and of course they can be null. Unfortunately, when the client do a POST of an entity sending "" (empty string) for the enumeration (because it can be null), I've an error server side:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `server.model.enums.EngineType` from String "": value not one of declared Enum instance names: [DIESEL, METHANE, ELECTRIC]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `server.model.enums.EngineType` from String "": value not one of declared Enum instance names: [DIESEL, METHANE, ELECTRIC]
at [Source: (PushbackInputStream); line: 1, column: 153] (through reference chain: server.model.tickets.Ticket["engineType2"])
I understand the sense of the message and I can solve the problem creating a custom deserializer as this:
#Component
public class EngineTypeDeserializer extends JsonDeserializer<EngineType> {
#Override
public EngineType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
return EngineType.valueOfNullable(node.asText());
}
}
but I should put this annotation #JsonDeserialize(using = EngineTypeDeserializer.class) in all EngineType fields in my beans.
I was looking for a better way to solve this problem. Do you have some advice?
You can register your custom serializer programmatically.
In your #Configuration class:
#Bean
#Primary // Use this to shadow other objectmappers, if anny
public ObjectMapper objectMapper(){
ObjectMapper objMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(EngineType.class, new EngineTypeDeserializer());
objMapper.registerModule(module);
}

how to tell jackson to put unrecognized json data into a particular field while deserializing

My data model/POJO:
public class MyPojo {
#JsonProperty("description")
protected String content;
#JsonProperty("name")
protected String title;
#JsonProperty("property")
protected List<Property> property;
#JsonProperty("documentKey")
protected String iD;
// Getters and setters...
}
However, my server returns json response in the following format.
{
"documentKey": "J2D2-SHRQ1_2-55",
"globalId": "GID-752726",
"name": "SHReq - Textual - heading test",
"description": "some text",
"status": 292,
"rationale$58": "Value of rationale",
"remark": "Just for testing purposes",
"release": 203
}
Here I've mapped the documentKey to iD and name to title of MyPojo. However, while using jackson's ObjectMapper, I get an exception saying globalId isn't recongnized.
The problem here is that it should put all such data fields (globalId, status, remark, release etc.) into the list of properties(List<Property> property). So I shouldn't tell jackson to ignore those.
How can I do that?
I think you will need to use a custom Deserializer. This way you have total control on how to arrange the data
class MyPojoDeserializer extends StdDeserializer<MyPojo> {
public MyPojoDeserializer() {
this(null);
}
public MyPojoDeserializer(Class<?> vc) {
super(vc);
}
#Override
public MyPojo deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
MyPojo myPojo = new MyPojo();
myPojo.setId(node.get("documentKey").asText());
myPojo.setContent(node.get("documentKey").asText());
myPojo.setTitle(node.get("name").asText());
// I just make a list of Strings here but simply change it to make a List<Property>,
// I do not know which Property class you want to use
List<String> properties = new ArrayList<>();
properties.add(node.get("globalId").asText());
properties.add(node.get("status").asText());
properties.add(node.get("rationale$58").asText());
properties.add(node.get("remark").asText());
properties.add(node.get("release").asText());
myPojo.setProperty(properties);
return myPojo;
}
}
Then add the following annotation to your MyPojo class
#JsonDeserialize(using = MyPojoDeserializer.class)
public class MyPojo {
protected String id;
protected String content;
protected String title;
protected List<Property> property;
// Getters/Setters
}
Then a classic readValue call should work
MyPojo myPojo = new ObjectMapper().readValue(json, MyPojo.class);

Polymorphic Deserialization Issue with Jackson

I have the following classes that I want to deserialize a JSON string to using Jackson.
PushNotificationMessage.java
public class PushNotificationMessage {
#JsonProperty("device_info")
private DeviceInfo deviceInfo;
private String content;
//getters & setters
}
DeviceInfo.java
public class DeviceInfo {
#JsonProperty(value = "device_type")
private String deviceType;
//getters & setters
}
IOSDeviceInfo.java
public class IOSDeviceInfo extends DeviceInfo {
#JsonProperty(value = "device_id")
private String deviceId;
private String arn;
#JsonProperty(value = "user_data")
private String userData;
//getters & setters
}
WebDeviceInfo.java
public class WebDeviceInfo extends DeviceInfo {
private String endpoint;
private String key;
private String auth;
//getters & setters
}
I have the following JSON content that I want to deserialize:
{
"device_info": {
"endpoint": "https://android.googleapis.com/gcm/send/blah",
"key": "blahkey",
"auth": "blahauth",
"device_type": "web"
},
"content": "Notification content"
}
I simply use ObjectMapper to try to perform the deserialization as such.
final ObjectMapper objectMapper = new ObjectMapper();
final PushNotificationMessage message = objectMapper.readValue(jsonString, PushNotifictionMessage.class);
When I do this I get:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "endpoint" (class com.blah.DeviceInfo), not marked as ignorable (one known property: "device_type"])
How can I get Jackson to recognize that it needs to be mapped to a WebDeviceInfo instance, instead of trying to map it to the DeviceInfo superclass, which does not have the endpoint field?
I've tried playing with #JsonTypeInfo and #JsonSubTypes annotations in my different classes, but I can find no good examples of how to use them.
EDIT: I added the #JsonDeserialize(using = DeviceInfoDeserializer.class) annotation to my DeviceInfo class, and created the following DeviceInfoDeserializer.
public class DeviceInfoDeserializer extends JsonDeserializer<DeviceInfo> {
private static final String DEVICE_TYPE = "device_type";
private static final String WEB = "web";
private static final String IOS = "ios";
#Override
public DeviceInfo deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
final ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec();
final ObjectNode root = objectMapper.readTree(jsonParser);
if (root.has(DEVICE_TYPE)) {
final JsonNode jsonNode = root.get(DEVICE_TYPE);
if (jsonNode.asText().equalsIgnoreCase(WEB)) {
return objectMapper.readValue(root.toString(), WebDeviceInfo.class);
} else if (jsonNode.asText().equalsIgnoreCase(IOS)) {
return objectMapper.readValue(root.toString(), IOSDeviceInfo.class);
}
}
throw deserializationContext.mappingException("Failed to de-serialize device info, as device_type was not \"web\" or \"ios\"");
}
}
Now, I get a different error when attempting to deserialize my PushNotificationMessage JSON:
java.lang.StackOverflowError: null
at com.fasterxml.jackson.databind.deser.std.BaseNodeDeserializer.deserializeObject(JsonNodeDeserializer.java:210)
at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:69)
at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:15)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:3770)
at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:2207)
at com.blah.serialization.DeviceInfoDeserializer.deserialize(DeviceInfoDeserializer.java:25)
at com.blah.serialization.DeviceInfoDeserializer.deserialize(DeviceInfoDeserializer.java:16)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
... (above trace repeated many times)
EDIT: Just needed to add #JsonDeserialize(as = WebDeviceInfo.class) and #JsonDeserialize(as = IOSDeviceInfo.class) to my subclasses, now it works as expected. Big thank you to #Luciano van der Veekens.
Jackson is not aware of polymorphism, it just tries to create an instance of the concrete DeviceInfo class.
However, you can implement a custom deserializer that programmatically parses the device info JSON and knows when to instantiate one of the subclasses due to the uniqueness of some fields such as endpoint.
https://fasterxml.github.io/jackson-databind/javadoc/2.2.0/com/fasterxml/jackson/databind/annotation/JsonDeserialize.html
#JsonDeserialize(using = DeviceInfoDeserializer.class)
public class DeviceInfo {
}
An example can be found here: http://sunilkumarpblog.blogspot.nl/2015/12/javajson-polymorphic-serialization-de.html

Spring MVC #RequestBody map Optional<Enum>

I have a rest controller with this method:
#RequestMapping(value = "", method = { RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<?> add(#Valid #RequestBody MyModel myModel, Errors errors) {
...
return new ResponseEntity<SomeObject>(someObject, HttpStatus.OK);
}
In MyModel has a field isMeetingOrSale that is enum (MeetingSaleFlag):
public enum MeetingSaleFlag {
MEETING("MEETING"),
SALE("SALE");
private final String name;
private MeetingSaleFlag(String s) { name = s; }
public boolean equalsName(String otherName) {
return (otherName == null) ? false : name.equals(otherName);
}
public String toString() { return this.name; }
}
and it can map a json that has a field "isMeetingOrSale" : "MEETING"
but the value in the json can be "isMeetingOrSale" : "" or completely missing, so in that case I want the field to be mapped to null. If I change the filed to be Optional<MeetingSaleFlag>
I got
Could not read JSON: Can not instantiate value of type [simple type,
class java.util.Optional<MeetingSaleFlag>] from String value
('MEETING'); no single-String constructor/factory method\\n at
[Source: java.io.PushbackInputStream#32b21158; line: 17, column: 18]
(through reference chain: MyModel[\"isMeetingOrSale\"]);
So the question is how can I map Optional enum from json?
Thanks to Sotirios Delimanolis's comment I was able to resolve the issue.
1) Add
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
as a dependency.
2) Reconfigure the Jackson mapper. Register:
#Bean
#Primary
public ObjectMapper jacksonObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
return mapper;
}
OR do this to register the jdk8 module
/**
* #return Jackson jdk8 module to be registered with every bean of type
* {#link ObjectMapper}
*/
#Bean
public Module jdk8JacksonModule() {
return new Jdk8Module();
}
Another way to customize Jackson is to add beans of type com.fasterxml.jackson.databind.Module to your context. They will be registered with every bean of type ObjectMapper, providing a global mechanism for contributing custom modules when you add new features to your application.
Doing this will only register the additional module and keep the built-in Jackson configuration provided by Spring Boot.
3) result
Now when the property is missing from the sent json, it's mapped to null
(This is not that great. I was expecting that it will give me an Optional and I will be able to use .isPresent()).
When it's an empty string ("isMeetingOrSale" : ""), Jackson returns an error:
Could not read JSON: Can not construct instance of
MyModel from String value '': value not
one of declared Enum instance names: [VAL1, VAL2]
which looks OK to me.
Useful links : Jackson jdk8 module, Spring MVC configure Jackson
This is an example from our codebase:
#NotNull // You probably don't want this
#JsonSerialize(using=CountrySerializer.class)
#JsonDeserialize(using=CountryDeserializer.class)
private CountryCode country;
where CountryCode is a complex enum (see nv-i18n) and these are the classes to (de)serialized from/to JSON:
public class CountrySerializer extends JsonSerializer<CountryCode> {
#Override
public void serialize(CountryCode value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeString(value.getAlpha3()); // Takes the Alpha3 code
}
public Class<CountryCode> handledType() { return CountryCode.class; }
}
and
public class CountryDeserializer extends JsonDeserializer<CountryCode> {
#Override
public CountryCode deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
// You can add here the check whether the field is empty/null
return CountryCode.getByCode(jp.getText());
}
}
You can easily replicate the same scenario using MeetingSaleFlag instead of CountryCode.

Jackson unrecognized field for class name

Following is how JSON string looks
{
"employee": {
"id": "c1654935-2602-4a0d-ad0f-ca1d514a8a5d",
"name": "smith"
...
}
}
Now i am using ObjectMapper#readValue(jsonAsStr,Employee.class) to convert it to JSON.
My Employee class is as follows...
#XmlRootElement(name="employee")
public class Employee implements Serializable {
private String id;
private String name;
...
public Employee() {
}
#XmlElement(name="id")
public String getId() {
return id;
}
public void setId(String id) {
this.id= id;
}
#XmlElement(name="name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
...
}
The exception I am getting is
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:
Unrecognized field "employee" (class com.abc.Employee), not marked as
ignorable (12 known properties: , "id", "name", ... [truncated]])
I am not able to understand why "employee" is considered as a property. Am i wrong in assuming that only class members are considered as properties?
The problem is that a JSON Object { } maps to a Java class, and the properties in the JSON map to the Java properties. The first { } in your JSON (which you are trying to unmarshal to Employee), has a property employee, which Employee class does not have a property for. That's why you are getting the error. If you were to try and unmarshal only the enclosed { }
{
"id": "c1654935-2602-4a0d-ad0f-ca1d514a8a5d",
"name": "smith"
}
it would work as Employee has those properties. If you don't have control over the JSON, then you can configure the ObjectMapper to unwrap the root value
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
But the you might have another problem. The unwrapping is based on the annotation on the Employee class, either #JsonRootName("employee") or #XmlRootElement(name = "employee"). With the latter though, you need to make sure you have JAXB annotation support. For that, you need to have the jackson-module-jaxb-annotations, then register the module
mapper.registerModule(new JaxbAnnotationModule());
This applies for all your JAXB annotations you're using. Without this module, they won't work.
#peeskillet is right.
I was looking for a long time about how to use jax annotation to deserialize the json returned from server since I was getting UnrecognizedPropertyException as well.
Adding the following code fixed my problem:
mapper.registerModule(new JaxbAnnotationModule());
Follow below the entire code i used:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JaxbAnnotationModule());
List<PojoTO>response = mapper.readValue(result.readEntity(String.class), mapper.getTypeFactory().constructCollectionType(List.class, PojoTO.class));

Categories

Resources