What does Spring use to do deserialization? - spring

I have the following class:
data class Thing(val lines: List<String>)
The JSON representation is:
{
"lines": [
"something",
"something else"
]
}
Spring WebFlux can successfully parse this with the following:
// Parse the JSON as an object and return it.
request -> ServerResponse.ok().body(request.bodyToMono(Thing::class.java)
However, using Jackson directly with either of the following techniques fails:
val mapper = ObjectMapper()
val item = mapper.readValue<Thing>("""{"lines":["something","something else"]}""")
ServerResponse.ok().body(request.bodyToMono(Map::class.java)
.map { map ->
val mapper = ObjectMapper()
val tmp = mapper.convertValue(map, Thing::class.java)
}
The error is:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `Thing` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
I thought that Spring was using Jackson to do its transformation. And I thought that Jackson could cope with such basic transformations from JSON -> POJOs. Using a #JsonDeserialize class obviously works. So why does the first example work and the second not?

Spring uses Jackson but it registers custom handlers and modules when it creates its default instance of ObjectMapper.
Jackson has special handling for constructors that take a single argument. This was done to support classes like UUID and URI. To instruct Jackson to not use this technique, annotate your constructor with #JsonCreator.
data class Thing #JsonCreator constructor(val lines: List<String>)
I have not reviewed Spring's reactive code so I do not know what or if it does something to disable Jackson's special handling.

Related

Deserializing a Enum with Jackson in Kotlin

I'm trying to serialize and deserialize an enum with Jackson.
My enum:
public enum class Type {
#JsonProperty("Typ A")
TypeA,
#JsonProperty("Typ B")
TypeB,
}
Serializing Type.TypeA results in the desired outcome of "Typ A". However Deserializing "Typ A" results in the following error:
java.lang.IllegalArgumentException: No enum constant de.advisori.pzp.task.TaskType.Typ A
I have tried other variations that I found online, such as this:
public enum class Type (#JsonValue val value: String) {
TypeA("Typ A"),
TypeB("Typ B"),
}
but they all yield the same result. Serialization works, deserialization results in the error above.
How do I correctly deserialize an enum with Jackson?
If it makes any difference: I am using it in a Spring Boot RequestMapping as a #RequestParam and return value.
As #dnault pointed out, Jackson isn't used for deserialization here. #RequestParams are never treated as JSON, hence Jackson is never used on them.
Two possible solutions are:
Using Kotlins ability to use spaces in names:
public enum class Type { `Typ A`, `Typ B` } (suggested by #DodgyCodeException)
Using a explicitly defined converter: https://stackoverflow.com/a/69031139/12898394 (pointed in the right direction by #Michal Ziober
I don't think any annotations will work to change the enum values. For this you need to write your own Serializer and Deserializer.
You will likely want to do this:
Create a Serializer by subclassing StdSerializer
Create a Deserializer by subclassing StdDeserializer
If you intend on using the enum as a key in JSON you will need KeyDeserializer too
Create a Module to wrap these up that you can pass to the configuration of Jackson, for that you use SimpleModule
There are many tutorials for this, e.g. https://www.baeldung.com/jackson-deserialization

Jackson deserialization of missing JSON number value to 0.0 in Spring

I have defined a Kotlin data class like this:
#JsonIgnoreProperties(ignoreUnknown = true)
data class MandatoryLimits(
val upper: Double,
val lower: Double
)
as part of a compound object that my Spring service receives as a request body in a POST request. When I send a test request with one value missing, e.g. like this:
"specificationLimits": {
"lower": 1.6
}
then I receive a correctly deserialised object of Type MandatoryLimits, but the missing value is set to simply 0.0.
I would have expected to get a BAD REQUEST from the service, instead. Do I need to configure something in addition?
Thanks in advance for all your help!
To fail deserialization when a Kotlin primitive is null, you need to make sure the DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES Jackson configuration flag is active.
Below is an example of ObjectMapper bean configuration that will throw an error when it tries deserializing a null Kotlin primitive.
#Configuration
class JacksonConfig {
#Bean
fun objectMapper(): ObjectMapper {
return jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true)
}
}
As a workaround I defined the data class members as nullable and added an init Block to validate the presence of both values. In case of an error, I throw the response exception manually.

Given an assignment to return specific data using Springboot reactive but the JSON is really complicated

I am new to Springboot reactive
I was asked to call the following endpoint and return todays weather data only:
https://api.weather.gov/gridpoints/MLB/33,70/forecast
I believe I need to use something like this...
WebClient.create().get()
.uri("https://api.weather.gov/gridpoints/MLB/33,70/forecast")
.retrieve()
.bodyToMono(WeatherClass.class)
.block();
Do I need to map out an entire java object to match the JSON at the endpoint? is there an easy way to perhaps just grab the a certain piece of the JSON?
How would I handle something like the #context annotation in the JSON.
The WebClient in spring boot automatically uses Jackson's ObjectMapper to unmarshall json to a java object when the content type of the response is application/json. So there is no need to pull in any additional libraries or have to write any specific unmarshalling code, unless you want to use an alternate json-to-java library.
When using Jackson, you don't need to map every field in the json to your java object. You can annotate your java class with #JsonIgnoreProperties to inform jackson to ignore any properties that may appear in the json but do not have a matching field in your java object.
An example WeatherClass in which you want only the #context and forecastGenerator unmarshalled would look something like this
#JsonIgnoreProperties
public class WeatherClass {
private final List<Object> context;
private final WeatherProperties weatherProperties;
public WeatherClass(#JsonProperty("#context") List<Object> context,
#JsonProperty("properties") WeatherProperties weatherProperties) {
this.context = context;
this.weatherProperties = weatherProperties;
}
private class WeatherProperties {
private final String forecastGenerator;
private WeatherProperties(#JsonProperty("forecastGenerator") String forecastGenerator) {
this.forecastGenerator = forecastGenerator;
}
}
}
Note
#context seems to be an array that can contain multiple types (both objects and strings in your example). I've used Object to work around this but obviously isn't the most graceful solution but should be adequate to demonstrate how Jackson works
Alternatively, you can unmarshall the response to a JsonNode, which you can then use to traverse the structure of the json without converting it to a java object. For example
String forecastGenerator = WebClient.create().get()
.uri("https://api.weather.gov/gridpoints/MLB/33,70/forecast")
.retrieve()
.bodyToMono(JsonNode.class)
.block().get("properties").get("forecastGenerator").toString()
There are many other annotations provided by Jackson that can used to define how the unmarshaller functions. Too many to cover here. See Jackson Deserialisation Annotations

Spring Data Elastic - Java.Time.Instant class Jackson deserliization not working

Following is my WorkroomDTO:
#NotNull
private Instant createdOn;
#JsonInclude(JsonInclude.Include.NON_NULL)
private Instant changedOn;
As you can see i am using Java 8 Instant class.
In the elasticsearch Index server i store the following as JSON:
"createdOn": {
"nano": 877000000,
"epochSecond": 1579861613
},
"changedOn": {
"nano": 920000000,
"epochSecond": 1579861613
},
The problem is when i query the elasticsearch server to get me the workroom
return elasticsearchOperations.queryForPage(new NativeSearchQueryBuilder().withQuery(mainQuery)
.withPageable(elasticUtils.interceptPageable(searchDto.getPageable(), "name"))
.build(),
WorkroomDTO.class);
, i make a mapping of these fields to my WorkroomDTO i get the following exception:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.Instant` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"createdOn":{"nano":68000000,"epochSecond":1580127683}
FYI:
I have created a configuration file where is register explicitly the JavaTimeModule to the Object Mapper
#Configuration
public class JacksonConfiguration {
#Value("${application.serialization.include-type-key:true}")
private boolean includeTypeKey = true;
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.addHandler(new MissingTypeIdHandler());
if (includeTypeKey) {
mapper.setMixInResolver(new TypeKeyMixInResolver());
}
return mapper;
}
}
Need help!
Where does this data come from? Is it written by your application?
It seems that the used Jackson mapper does not have the jackson-datatype-jsr310 module registered.
On reading the data tries to find a constructor of Instant that can be used to create an Instant object. But Instant does not have a default constructor and the Mapper should use the Instant.ofEpochSecond(long, long) method. This page pretty declares the problem and shows how the Jackson Mapper is configured.
Storing an instant in this way, as an object with two properties, is not the right way for storing dates in Elasticsearch. You should read the Elasticsearch documentation about how Elastcisearch handles date/time fields. When storing the instant as an object like this, you loose the ability to use Elasticsearch queries with criteria based on a date/time.
Which version of Spring Data Elasticsearch do you use? Because of problems like this, from the upcoming version 4.0 on, Spring Data Elasticsearch will not use the Jackson mapper anymore for entity mapping. The MappingElasticsearchConverter supports the use of the Elasticsearch date format and the java.time classes.
Well if I'm not completely wrong, your mapping fails due to the wrong format. The json you get looks like this:
"createdOn": {
"nano": 877000000,
"epochSecond": 1579861613
},
"changedOn": {
"nano": 920000000,
"epochSecond": 1579861613
},
That means you have 2 objects 'createdOn' and 'changedOn' with two properties (nano, epochSecond), while you try to map it to one object containing two properties named 'createdOn' and 'changedOn'. You need to modify that, you have e.g. a class called Entry, with two properties (nano, epochSeconds) and then a class with two properties (createdOn, changedOn) of type Entry

Jackson deserializer priority?

I have a Spring Boot app that is modeling ActityStreams objects and for the most part Jackson's Polymorphic Deserialization works well.
There are 'objects' in the JSON which are references (links) and not JSON objects with type information. For instance
"actor":"https://some.actors.href/ rather than
"actor":{
"type":"Actor",
"name":"SomeActor"
}
I've written custom deserializers and and placed them on the fields to deal with this
#JsonDeserialize (using = ActorOrLinkDeserializer.class)
private Actor actor;
However my ActorOrLinkDeserializer is instantiated but never called and Jackson complains with Missing type id when trying to resolve subtype of [simple type, class org.w3.activity.streams.Actor]: missing type id property 'type' (for POJO property 'actor') which is from the polymorphic deserializer.
It appears that the polymorphic deserialization code takes precedence over my local #JsonDeserialize annotation and I need a way to force my code to run first.
I've tried using my own ObjectMapper rather than Boot's and there's no difference.
I'd appreciate pointers and suggestions.
It turns-out there's a fairly simple solution to this problem using a DeserializationProblemHandler.
What I've implemented that works for all test cases so far is
1.
objectMapper.addHandler(new DeserProblemHandler());
or register with Spring Boot.
2.
public class DeserProblemHandler extends DeserializationProblemHandler {
public JavaType handleMissingTypeId(DeserializationContext ctxt, JavaType baseType, TypeIdResolver idResolver, String failureMsg) {
return TypeFactory.defaultInstance().constructType(baseType.getRawClass());
}
}
Add a constructor to each of the polymorphic classes that takes a string argument which is the href.

Resources