Jackson vs. Spring HATEOAS vs. Polymorphism - java

When I want to deserialize an Entity with a polymorph member, Jackson throws a com.fasterxml.jackson.databind.JsonMappingException, complaining about a missing type info (...which is actually present in the JSON -> see example).
Unexpected token (END_OBJECT), expected FIELD_NAME: missing property '#class' that is to contain type id (for class demo.animal.Animal)\n at [Source: N/A; line: -1, column: -1] (through reference chain: demo.home.Home[\"pet\"])"
All actual work is done by a PagingAndSortingRepository from Spring HATEOAS.
I use spring-boot V 1.2.4.RELEASE, which means jackson is V 2.4.6 and Spring HATEOAS is V 0.16.0.RELEASE.
Example:
I have a pet at home:
#Entity
public class Home {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#OneToOne(cascade = {CascadeType.ALL})
private Animal pet;
public Animal getPet() {
return pet;
}
public void setPet(Animal pet) {
this.pet = pet;
}
}
That Pet is some Animal - in this case either a Cat or a Dog. It's type is identified by the #class property...
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#class")
public abstract class Animal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
#Entity
public class Cat extends Animal {
}
#Entity
public class Dog extends Animal {
}
Then there is this handy PagingAndSortingRepository, which allows me to access my home via REST/HATEOAS...
#RepositoryRestResource(collectionResourceRel = "home", path = "home")
public interface HomeRepository extends PagingAndSortingRepository<Home, Integer> {
}
To confirm all that stuff is working, I have a test in place...
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = DemoApplication.class)
#WebAppConfiguration
public class HomeIntegrationTest {
#Autowired
private WebApplicationContext ctx;
private MockMvc mockMvc;
#Before
public void setUp() {
this.mockMvc = webAppContextSetup(ctx).build();
}
#Test
public void testRename() throws Exception {
// I create my home with some cat...
// http://de.wikipedia.org/wiki/Schweizerdeutsch#Wortschatz -> Büsi
MockHttpServletRequestBuilder post = post("/home/")
.content("{\"pet\": {\"#class\": \"demo.animal.Cat\", \"name\": \"Büsi\"}}");
mockMvc.perform(post).andDo(print()).andExpect(status().isCreated());
// Confirm that the POST request works nicely, so the JSON thingy is correct...
MockHttpServletRequestBuilder get1 = get("/home/").accept(MediaType.APPLICATION_JSON);
mockMvc.perform(get1).andDo(print()).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$._embedded.home", hasSize(1)))
.andExpect(jsonPath("$._embedded.home[0].pet.name", is("Büsi")));
// Now the interesting part: let's give that poor kitty a proper name...
MockHttpServletRequestBuilder put = put("/home/1")
.content("{\"pet\": {\"#class\": \"demo.animal.Cat\", \"name\": \"Beauford\"}}");
mockMvc.perform(put).andDo(print()).andExpect(status().isNoContent());
// PUT will thow JsonMappingException exception about an missing "#class"...
MockHttpServletRequestBuilder get2 = get("/home/").accept(MediaType.APPLICATION_JSON);
mockMvc.perform(get2).andDo(print()).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$._embedded.home", hasSize(1)))
.andExpect(jsonPath("$._embedded.home[0].pet.name", is("Beaufort")));
}
}
Interestingly I can create my home with the cat as a pet, but when I want to update the name of the cat it cannot deserialize the JSON anymore...
Any suggestions?

I'm going to attempt a half-answer.
When processing a PUT (probably PATCH as well), spring-data-rest-webmvc merges the given JSON data into the existing entity. While doing so, it strips all properties that don't exist in the entity from the JSON tree before passing it to the Jackson ObjectMapper. In other words, your #class property is gone by the time Jackson gets to deserialize your object.
You can work around this (for testing/demonstration purposes) by adding your #class property as an actual property to your entity (you have to rename it of course, say classname). Now everything will work fine, however your entity now has an otherwise useless classname property, which is probably not what you want.
Using the #JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.WRAPPER_OBJECT) approach also won't work, for a similar reason (except this time the entire wrapper object is removed). Also as with the original approach, GET and POST will work fine.
The whole thing looks like a bug or #JsonTypeInfo not supported in spring-data-rest-webmvc situation.
Maybe somebody else can shed some more light on this.

Related

How to deserialize json data and get rid of all the excess information

I'm having a problem with deserializing json object into DTO class.
The dto object has the following structure:
#Getter
#Setter
#ToString
#RequiredArgsConstructor
#JsonIgnoreProperties(value = { "charsetinfo" })
public class SingleEngineJsonDto {
public List<SingleEngineDTO> engine;
public List<Timetable> timetable;
public List<Dailytable> dailytable;
}
And the output is:
[SingleEngineJsonDto(engine=null, timetable=null, dailytable=null),
SingleEngineJsonDto(engine=[SingleEngineDTO(name=state, … some data)],
timetable=[Timetable(weekDay=1,some data), more data],
dailytable=[Dailytable(date=2018-05-09, more data), more data])]
How do I get rid of this([SingleEngineJsonDto(engine=null, timetable=null, dailytable=null)) part? Original json contains metadata, that I don't need. The firs object in a big json object is ignored metadata and null fields, the second one is null fields filled in. The only idea that I have is list.get(1). And I was told that this solution is wrong.
UPDATE:
Original json structure:
[{"charsetinfo":{"name": "utf-8"}},{"engine":
[{"NAME": "stock","title": some data}],
"timetable":[{"week_day":1,"is_work_day":1,some data},
more data],"dailytable":[{"date": "2018-05-09","is_work_day":0,"start_time": "10:00:00",data}]}]
Desirialization:
#FeignClient(value = "engine", url = "engine")
public interface EngineClient {
#GetMapping
List<EngineJsonDto> getEngines(URI enginesUri,
#RequestParam(value = "lang", required = false) String lang);
#GetMapping
List<SingleEngineJsonDto> getEngine(URI engineUri, #RequestParam(value = "lang", required = false) String lang);
}
Service gets the data from client and gets a list of dto to work with. I use facory pattern to get pieces of data to work with parameters(engine,timetable,dailytable), and all of them had to look the same:
#Service
public class EngineParamEngine implements EngineFactoryInterface {
#Override
public List<SingleEngineDTO> getEngineObjectPart(List<SingleEngineJsonDto> list){
return list.get(1).getEngine(); //<<this
}
}
I was told to change it to list.get(0).Which gives me (engine=null, timetable=null, dailytable=null). So, I need to make the first object disappear somehow. I'm sorry, English is not my first language and I'm new to programming.

How to ignore unknown fields on field level?

I am using a class from another module in my request.
public class KeyInput {
#NotNull
private Long id;
#NotNull
private String startValue;
#NotNull
private String endValue;
}
I cannot put #JsonIgnoreProperties(ignoreUnknown = true) annotation on this class, since the module does not contain jackson library.
Putting it on field level where I used it on my request class didn't work out.
#JsonIgnoreProperties(ignoreUnknown = true)
private List<KeyInput> keys;
Here is the incoming request. Notice the source of the problem, the two fields (name and type), which are not declared in KeyInput class.
{
"id": 166,
"name": "inceptionDate",
"type": "DATE",
"startValue": "22",
"endValue": "24"
}
How am I supposed to tell the jackson to ignore the unknown fields if the class is not in my package?
P.S: I know I can get the keys as json string and serialize it using ObjectMapper (by setting the configuration DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES to false), but I am looking for a cleaner solution here.
Also putting those fields in my class and never use it, is another dirty solution.
Two ways come to my mind.
Method 1
Create an empty child class that inherit from KeyInput class. This is the easiest method.
#JsonIgnoreProperties(ignoreUnknown = true)
public class InheritedKeyInput extends KeyInput{}
Method 2
Create a custom de-serializer for KeyInput class.
public class KeyInputDeserializer extends JsonDeserializer<KeyInput> {
#Override
public KeyInput deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
KeyInput keyInput = new KeyInput();
keyInput.setId(node.get("id").asLong());
keyInput.setEndValue(node.get("startValue").textValue());
keyInput.setStartValue(node.get("startValue").textValue());
return keyInput;
}
}
Bind this de-serializer to KeyInput using a configuration class
#Configuration
public class JacksonConfig implements Jackson2ObjectMapperBuilderCustomizer {
#Override
public void customize(Jackson2ObjectMapperBuilder builder) {
builder.failOnEmptyBeans(false)
.deserializerByType(KeyInput.class, new KeyInputDeserializer());
}
}
just simple add on above filed which you wanna ignore #JsonIgnore

Jackson Mixin: to deserialize routeId

I am using Jackson Mixin to deserialize mongo object and the Mixin looks as below.
public interface MyMixin {
/**
* Mixin to set key value for pojo.
* #param key key
* #param value value
*/
#JsonAnySetter
void put(String key, Object value);
}
The mixin works really well for all fields including list except for the id field.
The pojo looks as below.
public class MyPojo {
#JsonProperty("_id")
#javax.persistence.Id
private String id;
private String name;
}
The exception
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of java.lang.String out of START_OBJECT token
at [Source: (String)"{"_id": {"$oid": "5e049cb30eb7811fec0c7029"}
I understand that its failing to deserialize string from a nested json.
One way through which I solved the problem is via writing custom mapper
public static MyPojo map(final Document document) {
final MyPojo pojo = new MyPojo();
pojo.setUd(document.getObjectId("_id").toHexString());
// set other fields
return pojo;
But this requires change is mapper whenever there is a change in pojo, adding/removing fields.
The Mixin works without any change, How shall I update my Mixin to handle objectId as well?
I am not sure if you can have a mixin to handle this kind of case in a generic way. I propose a solution that has not much to do with mixin but gives one generic way to deserialize such fields. First create a class to represent the json id:
#Getter #Setter
public class OidId {
#JsonProperty("$oid")
private String oid;
}
Then an abstract class to have the mapping stuff:
public abstract class HasOidId {
// moved from MyPojo
#JsonProperty("_id")
#javax.persistence.Id
private String id;
#JsonSetter("_id") // this is the trigger for mapping "_id" stufff
public void setId(OidId oidId) {
this.id = oidId.getOid();
}
}
Then let MyPojo extend above:
#Getter #Setter
public class MyPojo extends HasOidId {
private String name;
}

Deserialize WRAPPER_OBJECT key as property using Jackson

I'm trying to deserialize this json data into list of objects:
[{
"a": {
"commonField": 1,
"aField": "AAA"
}
}, {
"b": {
"commonField": 2,
"bField": "BBB"
}
}]
Each object may be one of several types having both common and unique fields. Information about exact shape of an object is stored in json as key in the wrapper object.
I created corresponding classes for every known shape (set of unique fields) extending class containing all common fields. Also, I added Jackson annotations to the classes to enable polymorphic deserialization. Simplified, resulting classes look like this:
#JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(KeyBasedSubTypeA.class),
#JsonSubTypes.Type(KeyBasedSubTypeB.class)
})
public abstract class KeyBasedSuperType {
public String type;
public int commonField;
}
#JsonTypeName("a")
public class KeyBasedSubTypeA extends KeyBasedSuperType {
public String aField;
}
#JsonTypeName("b")
public class KeyBasedSubTypeB extends KeyBasedSuperType {
public String bField;
}
With this setup Jackson works almost perfectly. It is able to choose correct subtype during deserialization and populate all the fields including common and unique. However, the type field is not updated by Jackson, the key value used for selecting subtype is not stored anywhere. In other words, the data is deserialized into following structure:
[KeyBasedSubTypeA { type=null; commonField=1; aField=AAA },
KeyBasedSubTypeB { type=null; commonField=2; bField=BBB }]
Note type field having null value. So, the question is - How can I make Jackson to store wrapper's key used for selecting subtype somewhere in resulting object?
Here is my JUnit test for the process
public class PolymorphicTest {
private static ObjectMapper mapper;
#BeforeClass
public static void init() {
mapper = new ObjectMapper();
}
#Test
public void testKeyDenominator() throws IOException {
TypeReference<List<KeyBasedSuperType>> dataShape =
new TypeReference<List<KeyBasedSuperType>>() {};
List<KeyBasedSuperType> result = mapper.readValue(
PolymorphicTest.class.getResourceAsStream("polymorphic-key.json"), dataShape);
assertEquals(2, result.size());
assertEquals(KeyBasedSubTypeA.class, result.get(0).getClass());
assertEquals(KeyBasedSubTypeB.class, result.get(1).getClass());
assertEquals(1, result.get(0).commonField);
assertEquals(2, result.get(1).commonField);
assertEquals("a", result.get(0).type); // <---- this line fails
assertEquals("b", result.get(1).type); // <---- this line fails
assertEquals("AAA", ((KeyBasedSubTypeA) result.get(0)).aField);
assertEquals("BBB", ((KeyBasedSubTypeB) result.get(1)).bField);
}
}
The solution actually was very close, just missed a tiny step forward. It is #JsonTypeInfo(visible=true) required to make Jackson handle type info as normal property.
#JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT, property = "type", visible = true)
#JsonSubTypes({
#JsonSubTypes.Type(KeyBasedSubTypeA.class),
#JsonSubTypes.Type(KeyBasedSubTypeB.class)
})
public abstract class KeyBasedSuperType {
public String type;
public int commonField;
}

Tapestry JPA Jackson Deserialization

I'm working on a project I didn't initially create, in which the data was stored in-memory. I'm curently moving this data into the database. I'm doing this using hibernate and tapestry JPA. At some point in the project Jackson Deserialization is used (actually in connection with a UI, but I doubt that's relevant), via the #JsonDeserialize annotation, with a deserializer class (let's call it DefinitionDeserializer). DefinitionDeserializer then creates an instance of a POJO representation (let's call it Definition) of a database table (D_DEFINITION). However, D_DEFINITION has a connection to another table (D_TYPE) (and hence another POJO (PeriodType)). To resolve this connection, I'm using a tapestry service (ConnectingService), which I usually inject via the #Inject annotation. However, I can't use this method of injection when the object (in which I'm trying to inject the service, i.e. DefinitionDeserializer) was created via the new keyword - which seems to be the case for the #JsonDeserialize annotation. I also can't use ConnectingService without injecting it via the #Inject keyword, because then I couldn't inject any other services in ConnectingService either, which I'm currently doing.
I'm hoping this description didn't confuse you too much, I can't share the actual code with you and I don't think a minimal example would be much better, as it's quite a complicated case and wouldn't be such a small piece of code. If you need one, however, I can try to provide one.
Basically what I need is a way to tell JsonDeserialize to take a tapestry service instead of creating an instance of DefinitionDeserializer itself.
Edit: The classes as examples:
public DefinitionDeserializer extends StdDeserializer<Definition> {
private static final long serialVersionUID = 1L;
//TODO: The injection doesn't work yet
#Inject
private ConnectingService connectingService;
public DefinitionDeserializer() {
this(null);
}
public DefinitionDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Definition deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Definition pd = new Definition();
JsonNode node = p.getCodec().readTree(p);
if (node.has("type"))
pd.setType(periodTypeDao.findByValue("PeriodType." + node.get("type").asText()));
return pd;
}
}
#Entity
#Table(name = Definition.TABLE_NAME)
#Cacheable
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region =
JpaEntityModelConstants.CACHE_REGION_ADMINISTRATION)
public class Definition {
public static final String TABLE_NAME = "D_DEFINITION";
private static final long serialVersionUID = 389511526676381027L;
#Id
#SequenceGenerator(name = JpaEntityModelConstants.SEQUENCE_NAME, sequenceName = JpaEntityModelConstants.SEQUENCE_NAME, initialValue = 1, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = JpaEntityModelConstants.SEQUENCE_NAME)
#Column(name = "ID")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns({
#JoinColumn(name = "FK_TYPE", referencedColumnName = "ID")}
)
private PeriodType type;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public PeriodType getType() {
return type;
}
public void setType(PeriodType dpmType) {
this.type = dpmType;
}
//More columns
}
PeriodType looks pretty much the same as Definition.
//BaseService contains all the standard methods for tapestry JPA services
public interface ConnectingService extends BaseService<PeriodType> {
}
public class ConnectingServiceImpl extends BaseServiceImpl<PeriodType> implements ConnectingService {
public ConnectingServiceImpl() {
super (PeriodType.class);
}
}
Currently I'm using it like this (which doesn't work):
#JsonDeserialize(using = DefinitionDeserializer.class)
#JsonSerialize(using = DefinitionSerializer.class)
private Definition definition;
#JsonDeserialize doesn't create instances of deserialisers, it's just a hint for ObjectMapper to know which class to use when deserialising.
By default ObjectMapper uses Class.newInstance() for instantiating deserialisers, but you can specify custom HandlerInstantiator (ObjectMapper#setHandlerInstantiator()) in which you can use Tapestry's ObjectLocator to get instances of deserialisers, i.e. using ObjectLocator#autobuild(), or use ObjectLocator#getService() if your deserialisers are Tapestry services themselves.
Update:
public class MyHandlerInstantiator extends HandlerInstantiator
{
private final ObjectLocator objectLocator;
public MyHandlerInstantiator(ObjectLocator objectLocator)
{
this.objectLocator = objectLocator;
}
#Override
public JsonDeserializer<?> deserializerInstance(
DeserializationConfig config, Annotated annotated, Class<?> deserClass)
{
// If null returned here instance will be created via reflection
// you can always use objectLocator, or use it conditionally
// just for some classes
return objectLocator.autobuild(deserClass);
}
// Other method overrides can return null
}
then later when you're configuring ObjectMapper use #Injected instance of ObjectLocator to create an instance of custom HandlerInstantiator, i.e.:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setHandlerInstantiator(new MyHandlerInstantiator(objectLocator));
return objectMapper;

Categories

Resources