I currently facing an issue with the deserialization of json into an polymorphic type.
This is my controller which receives a RecommendedVirtualEditionParam.
#RequestMapping(
value = "/sortedEdition",
method = RequestMethod.POST,
headers = { "Content-type=application/json;charset=UTF-8"
})
public String getSortedRecommendedVirtualEdition(
Model model,
#RequestBody RecommendVirtualEditionParam params) {
//Do Stuff
}
RecommendedVirtualEditionParam is a container:
public class RecommendVirtualEditionParam {
private final String acronym;
private final String id;
private final Collection<Property> properties;
public RecommendVirtualEditionParam(
#JsonProperty("acronym") String acronym,
#JsonProperty("id") String id,
#JsonProperty("properties") Collection<Property> properties) {
this.acronym = acronym;
this.id = id;
this.properties = properties;
}
//Getters
}
Property is a polymorphic type and I believe it's the one giving my problems.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = SpecificTaxonomyProperty.class, name = "specific-taxonomy")
})
public abstract class Property {
public Property() {
}
//Other methods
}
The sub type:
public class SpecificTaxonomyProperty extends Property {
private final String acronym;
private final String taxonomy;
public SpecificTaxonomyProperty(
#JsonProperty("acronym") String acronym,
#JsonProperty("taxonomy") String taxonomy) {
this.acronym = acronym;
this.taxonomy = taxonomy;
}
The json being send on requests:
{
acronym: "afs"
id: "167503724747"
properties: [
{
type: "specific-taxonomy",
acronym: "afs",
taxonomy: "afs"
}
]
}
When I run it like this I get a org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:149) ~[spring-webmvc-3.2.2.RELEASE.jar:3.2.2.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:180) ~[spring-webmvc-3.2.2.RELEASE.jar:3.2.2.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:95) ~[spring-webmvc-3.2.2.RELEASE.jar:3.2.2.RELEASE]
I believe there is something wrong with with the way I setup my Property class that makes it unable to deserialize. Any has a clue and give me an hand?
I managed to fix my problem.
Before posting, I had looked around and a lot of answers pointed to the class I wanted to deserialize having multiple setters for the same attribute. I didn't pay much attention to it, since my attributes were final, I didn't have any setters for them.
https://stackoverflow.com/a/19444874/2364671
But I had a method called setUserWeight(RecommendedWeights weights) which didn't set any attributes of Property, but after renaming it, my problem was fixed.
I don't quiet understand the reason for this odd behavior and would love some light about it.
Related
I have an class hierarchy as below
public class Car {
private String make;
private String model;
#Schema(example = "MANUAL")
private TransmissionType transmissionType;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "transmissionType")
private Transmission transmission;
}
#JsonSubTypes({
#JsonSubTypes.Type(value = AutomaticTransmission.class, name = "AUTOMATIC"),
#JsonSubTypes.Type(value = ManualTransmission.class, name = "MANUAL"))
})
public abstract class Transmission {
}
public class AutomaticTransmission {
#Schema(example = "DCT")
public Technology technology;
}
public class ManualTransmission {
#Schema(example = "5")
public int numGears;
}
Now, when swagger is generated, I see that for car model,
{
"transmissionType": "MANUAL"
"transmission": {
"technology": "DCT"
}
}
Here transmission type is manual but example given of automatic. Requirement is give example of manual transmission. How do I link these two properties.
I know that I can create an example json and put it in #Schema(example = "{\"numGears\": 5}") but this will create maintenance overhead of modifying json when class is modified.
I am trying to deserialize JSON into a custom POJO that I am not able to modify. That POJO has annotations from a different custom internal serialization framework that I'm not able to use. How can I create a custom deserializer that will respect these annotations?
Here is an example POJO:
public class ExampleClass {
#Property(name = "id")
public String id;
#Property(name = "time_windows")
#NotNull
public List<TimeWindow> timeWindows = new ArrayList<>();
public static class TimeWindow {
#Property(name = "start")
public Long start;
#Property(name = "end")
public Long end;
}
}
So in this case, the deserializer would look for fields in the JSON that correspond to the Property annotations, and use the value in that annotation to decide what field to grab. If a property doesn't have the Property annotation, it should be ignored.
I have been going through the Jackson docs but haven't been able to find exactly what I need. Is this a place where an AnnotationIntrospector would be useful? Or possibly a ContextualDeserializer?
Any pointers in the right direction would be greatly appreciated!
Update: I tried implementing the advice in the comments, but without success.
Here is my initial implementation of the introspector:
class CustomAnnotationInspector : JacksonAnnotationIntrospector () {
override fun hasIgnoreMarker(m: AnnotatedMember?): Boolean {
val property = m?.getAnnotation(Property::class.java)
return property == null
}
override fun findNameForDeserialization(a: Annotated?): PropertyName {
val property = a?.getAnnotation(Property::class.java)
return if (property == null) {
super.findNameForDeserialization(a)
} else {
PropertyName(property.name)
}
}
}
And here is where I actually use it:
// Create an empty instance of the request object.
val paramInstance = nonPathParams?.type?.getDeclaredConstructor()?.newInstance()
// Create new object mapper that will write values from
// JSON into the empty object.
val mapper = ObjectMapper()
// Tells the mapper to respect custom annotations.
mapper.setAnnotationIntrospector(CustomAnnotationInspector())
// Write the contents of the request body into the StringWriter
// (this is required for the mapper.writeValue method
val sw = StringWriter()
sw.write(context.bodyAsString)
// Deserialize the contents of the StringWriter
// into the empty POJO.
mapper.writeValue(sw, paramInstance)
Unfortunately it seems that findNameForDeserialization is never called, and none of the JSON values are written into paramInstance. Can anybody spot where I'm going wrong?
Thank you!
Update 2: I changed the code slightly, I'm now able to identify the property names but Jackson is failing to create an instance of the object.
Here's my new code:
val mapper = ObjectMapper()
// Tells the mapper to respect CoreNg annotations.
val introspector = CustomAnnotationInspector()
mapper.setAnnotationIntrospector(introspector)
val paramInstance = mapper.readValue(context.bodyAsString,nonPathParams?.type)
My breakpoints in the custom annotation introspector are getting hit. But I'm getting the following exception:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `app.employee.api.employee.BOUpsertEmployeeRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
Here is the POJO I'm trying to deserialize:
public class BOUpsertEmployeeRequest {
public BOUpsertEmployeeRequest () { }
#NotNull
#Property(name = "xref_code")
public String xrefCode;
#Property(name = "first_name")
public String firstName;
#Property(name = "last_name")
public String lastName;
#Property(name = "email_address")
public String emailAddress;
#Property(name = "phone")
public String phone;
#Property(name = "address")
public List<String> address;
#Property(name = "employment_status")
public String employmentStatus;
#Property(name = "pay_type")
public String payType;
#Property(name = "position")
public String position;
#Property(name = "skills")
public List<String> skills;
#Property(name = "gender")
public String gender;
}
As far as I can tell it has a default constructor. Anybody have any idea what the problem is?
Thank you!
Method hasIgnoreMarker is called not only for fields, but also for the constructor, including the virtual one:
Method called to check whether given property is marked to be ignored. This is used to determine whether to ignore properties, on per-property basis, usually combining annotations from multiple accessors (getters, setters, fields, constructor parameters).
In this case you should ignore only fields, that are not marked properly:
static class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public PropertyName findNameForDeserialization(Annotated a) {
Property property = a.getAnnotation(Property.class);
if (property == null) {
return PropertyName.USE_DEFAULT;
} else {
return PropertyName.construct(property.name());
}
}
#Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
return m instanceof AnnotatedField
&& m.getAnnotation(Property.class) == null;
}
}
Example:
class Pojo {
// #Property(name = "id")
Integer id;
// #Property(name = "number")
Integer number;
#Property(name = "assure")
Boolean assure;
#Property(name = "person")
Map<String, String> person;
}
String json =
"{\"id\" : 1, \"number\" : 12345, \"assure\" : true," +
" \"person\" : {\"name\" : \"John\", \"age\" : 23}}";
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new CustomAnnotationIntrospector());
Pojo pojo = mapper.readValue(json, Pojo.class);
System.out.println(pojo);
Pojo{id=null, number=null, assure=true, person={name=John, age=23}}
Note: Custom Property annotation should have RetentionPolicy.RUNTIME (same as JsonProperty annotation):
#Retention(RetentionPolicy.RUNTIME)
public #interface Property {
String name();
}
I will suggest a different approach:
In the runtime, with the bytecode instrumentation library Byte Buddy and its Java agent, re-annotate the fields with the proper Jackson Annotations. Simply implement the logic via reflection. See the following example:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.dynamic.DynamicType.Builder.FieldDefinition.Valuable;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface MyJsonIgnore {
}
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface MyJsonProperty {
String name();
}
public class Sample {
public static void main(String[] args) throws JsonProcessingException {
ByteBuddyAgent.install();
ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.fromInstalledAgent();
ByteBuddy byteBuddy = new ByteBuddy();
AnnotationDescription jsonIgnoreDesc =
AnnotationDescription.Builder.ofType(JsonIgnore.class).build();
Builder<Person> personBuilder = byteBuddy.redefine(Person.class);
for (Field declaredField : Person.class.getDeclaredFields()) {
Valuable<Person> field = personBuilder.field(ElementMatchers.named(declaredField.getName()));
MyJsonProperty myJsonProperty = declaredField.getAnnotation(MyJsonProperty.class);
if (myJsonProperty != null) {
AnnotationDescription jsonPropertyDesc =
AnnotationDescription.Builder.ofType(JsonProperty.class)
.define("value", myJsonProperty.name())
.build();
personBuilder = field.annotateField(jsonPropertyDesc);
}
MyJsonIgnore myJsonIgnore = declaredField.getAnnotation(MyJsonIgnore.class);
if (myJsonIgnore != null) {
personBuilder = field.annotateField(jsonIgnoreDesc);
}
}
personBuilder.make().load(Sample.class.getClassLoader(), classReloadingStrategy);
Person person = new Person("Utku", "Ozdemir", "Berlin");
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(person);
System.out.println(jsonString);
}
}
class Person {
#MyJsonProperty(name = "FIRST")
private String firstName;
#MyJsonProperty(name = "LAST")
private String lastName;
#MyJsonIgnore private String city;
public Person(String firstName, String lastName, String city) {
this.firstName = firstName;
this.lastName = lastName;
this.city = city;
}
}
In the example above, I
create MyJsonProperty and MyJsonIgnore annotations and a Person class for the demonstration purpose
instrument the current Java process with the Byte buddy agent
create a bytebuddy builder to redefine the Person class
loop over the fields of the Person class and check for these annotations
add an additional annotation to each of those fields (on the builder), Jackson's JsonProperty (with the correct field name mapping) and JsonIgnore.
after being done with the fields, make the new class bytecode and load it to the current classloader using the byte buddy agent's class reloading mechanism
write a person object to the stdout.
It prints, as expected:
{"FIRST":"Utku","LAST":"Ozdemir"}
(the field city is ignored)
This solution might feel like an overkill, but on the other side, it is pretty generic solution - with a few changes in the logic, you could handle all the 3rd party classes (which you are not able to modify) instead of handling them case by case.
I have a list of POST requests, where request bodies are quite similar
{
"entity":{
"type":"Nissan"
"parts":{
"Nissan_unique_content1":"value",
"Nissan_unique_content2":"value"
}
}
"updateDate":"Date..."
}
{
"entity":{
"type":"Ford"
"parts":{
"Ford_unique_content1":"value",
"Ford_unique_content2":"value",
"Ford_unique_content3":"value"
}
}
"updateDate":"Date..."
}
I have a generic RequestBody
public class RequestBody<T>{
EntityBody<T> entity;
Date updateDate;
}
public class EntityBody<T>{
String type;
T parts;
}
In my Post Controller I have method as
#RequestMapping(value = "/{type}")
public ResponseEntity<?> create(
#PathVariable(value = "type") String type,
#RequestBody RequestBody<T> body) {
...
}
Is there anyway that generic type T can be assigned depends on type?
In this case I wouldn't need create multiple create method, otherwise I need create multiple method, like
#RequestMapping(value = "/nissan")
public ResponseEntity<?> createNissan(
#RequestBody RequestBody<NissanContent> body) {
...
}
#RequestMapping(value = "/ford")
public ResponseEntity<?> createFord(
#RequestBody RequestBody<Ford> body) {
...
}
which are unnecessary repetitions.
This can be done by using #JsonTypeInfo annotation.
For example:
Define entities according to different structures under "parts" key:
class NissanParams {
#JsonProperty("Nissan_unique_content1")
private String nissanUniqueContent1;
#JsonProperty("Nissan_unique_content2")
private String nissanUniqueContent2;
// getters + setters
}
In EntityBody, remove type field and add the annotations:
public class EntityBody<T> {
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
#JsonSubTypes({ #JsonSubTypes.Type(value = NissanParams.class, name = "Nissan")})
private T parts;
}
And there will be a single controller method:
#PostMapping(path = "{make}",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE)
public RequestBody<Object> create(#PathVariable("make") String make,
#org.springframework.web.bind.annotation.RequestBody RequestBody<Object> body) {
// please change the name of "RequestBody" entity, in order to avoid name clash with the annotation
}
You can use JsonTypeInfo and JsonSubTypes Jackson annotations. Your model could look like:
class EntityBody {
private Car parts;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
#JsonSubTypes({
#JsonSubTypes.Type(name = "Ford", value = Ford.class),
#JsonSubTypes.Type(name = "Nissan", value = Nissan.class)
})
public Car getParts() {
return parts;
}
}
As you can see, you do not need type property. It will be read by Jackson to find out a car type. I have created Car base class/interface but you do not need to do that.
Your POST method could look like this:
#RequestMapping(value = "/cars", method = RequestMethod.POST)
public ResponseEntity<?> create(#RequestBody RequestPayload body) {
logger.info(body.toString());
return ResponseEntity.ok("OK");
}
You do not need PathVariable here.
I'm trying to parse some XML that looks like this:
<correlationMatrix>
<assetMatrix numAssets="45">
<correlations asset="Name1" />
<correlations asset="Name2">
<correlation asset="Name3">1.23</correlation>
</correlations>
<correlations asset="Name4">
<correlation asset="Name5">2.34</correlation>
<correlation asset="Name6">3.45</correlation>
</correlations>
</assetMatrix>
</correlationMatrix>
I've created 3 classes:
#JsonIgnoreProperties(ignoreUnknown = true)
public class CorrelationMatrix {
private List<Correlations> assetMatrix;
public List<Correlations> getAssetMatrix() {
return assetMatrix;
}
public void setAssetMatrix(List<Correlations> assetMatrix) {
this.assetMatrix = assetMatrix;
}
}
And
#JsonIgnoreProperties(ignoreUnknown = true)
public class Correlations {
private String asset;
private List<Correlation> correlation;
public String getAsset() {
return asset;
}
public void setAsset(String asset) {
this.asset = asset;
}
public List<Correlation> getCorrelation() {
return correlation;
}
public void setCorrelations(List<Correlation> correlation) {
this.correlation = correlation;
}
}
Then finally
#JsonIgnoreProperties(ignoreUnknown = true)
public class Correlation {
}
As you can see I've removed everything from the final inner class, but it still fails to parse. I've tried removing <correlations asset="Name1" /> from the input but that's not the source of the problem. If I remove private List<Correlation> correlation; from Correlations then that does then parse successfully but obviously doesn't have the information I need.
What is it that I need to do differently here to parse what is essentially a 2 dimensional array from XML into Java using Jackson (2.2.0 if that matters)?
The error I get is:
Missing name, in state: START_OBJECT (through reference chain: CorrelationMatrix["assetMatrix"]->Correlations["correlation"])
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(
Update:
The problem seems to be associated with the values inside correlation. If I remove 1.23, 2.34 and 3.45 from my example data then it parses - so I need to somehow tell Jackson how to map them.
I was able to parse all the elements in the example xml with these modified classes (add getters, setters and use correct name setCorrelation in Correlations):
class CorrelationMatrix {
private AssetMatrix assetMatrix;
}
class AssetMatrix {
#JacksonXmlProperty(isAttribute = true)
private int numAssets;
#JacksonXmlElementWrapper(useWrapping = false)
private List<Correlations> correlations;
}
class Correlations {
#JacksonXmlProperty(isAttribute = true)
private String asset;
#JacksonXmlElementWrapper(useWrapping = false)
private List<Correlation> correlation;
}
class Correlation {
#JacksonXmlProperty(isAttribute = true)
private String asset;
#JacksonXmlText
private double correlation;
}
I didn't need #JsonIgnoreProperties(ignoreUnknown = true) anywhere
#JacksonXmlProperty(isAttribute = true) is needed for attributes like asset and numAssets
There are 2 types of lists in the xml that are both unwrapped so specify it with this #JacksonXmlElementWrapper(useWrapping = false)
You can parse the innermost double numbers with this #JacksonXmlText although the field in Java is not text.
I introduced a wrapper class AssetMatrix to capture numAssets
I am trying to have a REST endpoint create a subtype of Widget when POSTing to it,
here is the base class for all Widgets
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "widgetType")
#JsonSubTypes({
#JsonSubTypes.Type(value = TextWidget.class, name = WidgetType.Constants.TEXT),
#JsonSubTypes.Type(value = ImageWidget.class, name = WidgetType.Constants.IMAGE),
#JsonSubTypes.Type(value = IndicatorWidget.class, name = WidgetType.Constants.INDICATOR),
#JsonSubTypes.Type(value = MapWidget.class, name = WidgetType.Constants.MAP),
#JsonSubTypes.Type(value = ChartWidget.class, name = WidgetType.Constants.CHART)
})
#Data
#Slf4j
public abstract class Widget {
...
}
this is the WidgetType enum:
public enum WidgetType {
TEXT(Constants.TEXT),
IMAGE(Constants.IMAGE),
INDICATOR(Constants.INDICATOR),
MAP(Constants.MAP),
CHART(Constants.CHART);
private final String type;
WidgetType(final String type) {
this.type = type;
}
public static class Constants {
public static final String TEXT = "TEXT";
public static final String IMAGE = "IMAGE";
public static final String INDICATOR = "INDICATOR";
public static final String MAP = "MAP";
public static final String CHART = "CHART";
}
}
and this is my Spring endpoint:
#RequestMapping(method = RequestMethod.POST)
public Optional<Widget> createWidget(#Valid final Widget widget) {
...
}
when hitting that endpoint it throws this exception:
{
"timestamp": 1493029336774,
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.beans.BeanInstantiationException",
"message": "Failed to instantiate [....models.Widget]: Is it an abstract class?; nested exception is java.lang.InstantiationException",
"path": "...."
}
skimming through few solutions for my problem, I might have to manually register the subtypes, I might be wrong, but I think there must be a way to make it work with annotations, maybe I am missing something?
problem solved,
I was annotating my classes with Jackson annotation and forgot that I was sending multipart POST requests, that wasn't even going to Jackson.
The solution is as simple as this:
#RequestMapping(method = RequestMethod.POST)
public Optional<Widget> createWidget(#RequestBody final Widget widget) {
...
}