Jackson incorrectly deserializing nested object's values into parent - java

I have the following JSON file which I am trying to deserialize:
{
"name": "ComponentX",
"implements": ["Temperature.Sensor"],
"requires": [
{"type": "interface", "name": "Temperature.Thermostat", "execute": [
"../Thermostat.exe"
]}
]
}
It is a description of a component in a code sample for a distributed system.
Here is the class that this is supposed to deserialize to:
public class ComponentDescription {
#JsonProperty("name")
public String Name;
#JsonProperty("implements")
public String[] Implements;
#JsonProperty("requires")
public ComponentDependency[] Requires;
#JsonIgnore
public String RabbitConnectionName;
private static final ObjectMapper mapper = new ObjectMapper();
public static ComponentDescription FromJSON(String json)
throws JsonParseException, JsonMappingException, IOException
{
return mapper.readValue(json, ComponentDescription.class);
}
public class ComponentDependency
{
#JsonCreator
public ComponentDependency() {
// Need an explicit default constructor in order to use Jackson.
}
#JsonProperty("type")
public DependencyType Type;
#JsonProperty("name")
public String Name;
/**
* A list of ways to start the required component if it is not already running.
*/
#JsonProperty("execute")
public String[] Execute;
}
/**
* A dependency can either be on "some implementation of an interface" or it
* can be "a specific component", regardless of what other interface implementations
* may be available.
*/
public enum DependencyType
{
Interface,
Component
}
}
When I run ComponentDescription.FromJSON(jsonData), which uses the Jackson ObjectMapper to deserialize the JSON into the appropriate classes, I get the following exception:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "type" (class edu.umd.cs.seam.dispatch.ComponentDescription), not marked as ignorable (3 known properties: , "implements", "name", "requires"])
at [Source: java.io.StringReader#39346e64; line: 1, column: 103] (through reference chain: edu.umd.cs.seam.dispatch.ComponentDescription["requires"]->edu.umd.cs.seam.dispatch.ComponentDescription["type"])
It seems that Jackson is trying to deserialize the requires array in the JSON object as an array of ComponentDescription instead of an array of ComponentDependency. How do I get it to use the correct class? I would prefer an answer that gets Jackson to look at the type of public ComponentDependency[] Requires and see use it automatically over an answer that requires me to put the type name in again somewhere else (such as an # attribute).

My guess is that the problem comes from ComponentDependency not being static. As it is not declared static, it means it can only be instantiated with an existing instance of ComponentDescription.
For more details, see here.

Related

HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `int` from String

How to show conversion error along with other validation errors when creating REST API?
Spring Boot
Spring MVC
REST Controller
#Data
#AllArgsConstructor
class DTO
{
#NotNull
#Min(1)
private Integer id;
}
#RestController
class ExampleController
{
#PostMapping("/")
public void createEndpoint(#Valid #RequestBody DTO dto) {
return "{\"message\": \"OK\"}";
}
}
When I make request to this endpoint,
POST /
{
"id: "abrakadabra"
}
I would like to get something like this
{
"errors": {
"id": [
{ code: "invalid_format", message: "Field must contain only digits" }
]
}
}
What I actually get?
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `int` from String "abrakadabra": not a valid `int` value;
nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `int` from String "abrakadabra": not a valid `int` value
at [Source: (PushbackInputStream); line: 2, column: 24] (through reference chain: com.mylid.back.dtos.CreateCompanyDto["pricing_plan_id"])]
I know that I can create custom annotation for validation.
BUT the problem is that the process does not reach validation. It fails on deserialization step.
[Possible way]
There is one dirty trick, to change Integer type in DTO to String. Create custom annotation to check if String contains only digits. Then manually map this String from DTO to Integer in Entity. Then save to database.
What are other ways how to solve this problem?
It is very weird, that there is a few topics on Google and StackOverFlow for this particular problem. And on almost every page the accepted answer is "it is not our problem, API clients should pass integer".
In PHP we can easily do it with 'integer' validator, almost every framework has it, and it will not prevent other fields from validation if I pass "abracadabra" instead of "1".
What about implementing a custom type?
For example (the code below is crap, missing null handling and so on - just wanted to show you the idea)
public class DTOId {
final Integer value;
private DTOId(String value) {
Assert.hasText(value, "Value for DTOId should not not be null or empty!");
// Validation that it is a integer, min value and so on
this.value = Integer.valueOf(value);
}
#JsonCreator
public static DTOId of(final String value) {
return new DTOId(value);
}
#JsonValue
public String toString() {
return value.toString();
}
}
Then you could use
#Data
#AllArgsConstructor
class DTO
{
#NotNull
private DTOId id;
}

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.

Jackson's BeanDeserializerModifier does not work with final fields

I wrote a Custom Serializer and Custom Deserializer to serialize properties marked with #Confidential annotation.
#Data
public class Person {
private String name;
#Confidential
private String address;
}
The Custom Serializer serializes a POJO with following values:
{ "name": "John Doe", "address": "Kearney St"}
as follows:
{"name":"John Doe", "address": {"value":"IjIwMzEwIDU4dGggTG4gTkUi"}}
The Custom Deserializer is also able to deserialize the JSON back to the Person POJO fine.
However, when I make the fields in the Person POJO final, serialization continues to work, but deserialization fails.
#Data
public class Person {
private final String name;
#Confidential
private final String address;
}
Here's the BeanSerializerModifier implementation:
#AllArgsConstructor
public class CustomDeserializerModifier extends BeanDeserializerModifier {
private final ObjectMapper objectMapper;
#Override
public BeanDeserializerBuilder updateBuilder(final DeserializationConfig config,
final BeanDescription beanDesc,
final BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> beanPropertyIterator = builder.getProperties();
beanPropertyIterator.forEachRemaining(settableBeanProperty -> {
final Confidential annotation = settableBeanProperty.getAnnotation(Confidential.class);
if (encryptedProperty != null) {
JsonDeserializer<Object> current = settableBeanProperty.getValueDeserializer();
final SettableBeanProperty newSettableBeanProperty =
settableBeanProperty.withValueDeserializer(
new CustomDeserializer(annotation, current, objectMapper)
);
builder.addOrReplaceProperty(newSettableBeanProperty, true);
}
});
return builder;
}
}
I found that CustomDeserializer, never gets called when the Person POJO fields are final.
Here's the error message:
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: {"name":"John Doe","address":{"value":"IjIwMzEwIDU4dGggTG4gTkUi"}}; line: 1, column: 30] (through reference chain: com.custom.model.Person["address"])
Can a Jackson expert please tell me why my CustomDeserializer isn't getting invoked when the POJO fields are final.
Thank you!
As mentioned, serialization will perfectly work for both mutable and immutable fields. Deserialization issue will only occur when using immutable fields as the BeanDeserializerModifier won't work in such a case.
In the Jackson terminology, immutable fields are named creator properties, meaning they are initialized using creators. See BeanDeserializerBase#resolve.
To correctly handle this use case, the ObjectMapper may be created with a custom DeserializationContext (an extended implementation of the ObjectMapper may also set the protected field related to the deserialization context).
Then, through an override of the method DeserializationContext#handleSecondaryContextualization, it will be possible to change the deserialization to make it work.
There is maybe other possibilities but this one is working fine with encryption.

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 enum deserialization

I'm trying to deserialize json object using jackson and getting exception
`com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:
Unrecognized field "mobile" (class mypack.JacksonPhoneBuilder), not
marked as ignorable (2 known properties: , "phoneType", "value"]) at
[Source: %filelocation%; line: 8, column: 24] (through reference
chain:
mypack.JacksonAddressListBuilder["addresses"]->mypack.JacksonAddressBuilder["phones"]->mypack.JacksonPhoneBuilder["mobile"])`
This is the object:
{
"addresses": [
{
...
"phones": {
"mobile": "+01234567890"
}
},
...
]
}
Phone.java:
#JsonDeserialize(builder = JacksonBuilder.class)
public class Phone {
protected String value;
protected Type type;
// setters and getters
}
i've read about jackson enum deserializtion, but there was plain enum
and there was used Map. Obviously, field "mobile" is not
represented in model, but it's a enum value, so how can i deserialize
it?
Your JacksonPhoneBuilder works the same way as Jackson default deserialization. The problem is that it's able to read phones in following form:
{
"type": "mobile",
"value": "+01234130000"
}
However in your json object phones are represented as a subobject which can be seen in Java as a Map<PhoneType, String>. One of possible solutions is to use a Converter from Map to List (I assume there may be many phones in one address).
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;
public class PhoneConverter implements Converter<Map<PhoneType, String>, List<Phone>>{
public List<Phone> convert(Map<PhoneType, String> phonesMap) {
List<Phone> phones = new ArrayList<Phone>();
for (PhoneType phoneType : phonesMap.keySet()) {
phones.add(new Phone(phoneType, phonesMap.get(phoneType)));
}
return phones;
}
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructMapLikeType(Map.class, PhoneType.class, String.class);
}
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructCollectionLikeType(List.class, Phone.class);
}
}
Then in your Address class:
public class Address {
#JsonDeserialize(converter = PhoneConverter.class)
protected List<Phone> phones;
}
Note that it won't play with your Builders but if you don't do any other custom deserialization then you don't need them - you can rely on Jackson's default behavior.

Categories

Resources