I want to implement a custom Json mapping to the POJO below at class level.
#JsonDeserialize(using = DeviceDeserializer.class)
public class Pojo {
private String device;
private String port;
private String reservbleBw;
*default constructor, getter and setters*
}
Below is my Json File
[
{
"Device":"ABCD",
"ifces":[
{
"port":"ABCD:1/2/0",
"reservableBW":"1000",
"capabilites":[ "MPLS" ]
},
{
"port":"ABCD:1/2/1",
"reservableBW":"100",
"capabilites":[ "ETHERNET" ]
}
]
}
]
Now i only need to map the ports and reservableBw when 'capabilities' is 'ETHERNET'. I looked at few examples of custom deserializer but i do not know how to pass the value for JsonParser and DeserializationContext. I have problem understanding the below line.
public Pojo deserialize(JsonParser jParser, DeserializationContext ctxt) throws IOException, JsonProcessingException
{
// custom deserializer implementation
}
There are two methods to solve your issue:
Method 1: use a middle object to mapper: I have created a demo for your issue see this.
Create a middle object to mapper the original JSON
public class SfPojoDto {
private String device;
private List<Ifce> ifces;
}
public class Ifce {
private String port;
private String reservableBW;
private String[] capabilites;
}
Then use custom deserializer to mapper it firstly and convert to your target object.
public class DeviceDeserializer extends JsonDeserializer<SfPojo> {
#Override
public SfPojo deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// while (p.nextToken()!=null) {
// if (p.getCurrentToken()==)
// }
String temp = p.readValueAsTree().toString();
ObjectMapper mapper = new ObjectMapper();
SfPojoDto sfPojoDto = mapper.readValue(temp, SfPojoDto.class);
SfPojo sfPojo = new SfPojo();
sfPojo.setDevice(sfPojoDto.getDevice());
List<Ifce> ifceList = sfPojoDto.getIfces();
for (Ifce ifce : ifceList) {
List<String> capabilities = Arrays.asList(ifce.getCapabilites());
if (capabilities.contains("ETHERNET")) {
sfPojo.setPort(ifce.getPort());
sfPojo.setReservbleBw(ifce.getReservableBW());
return sfPojo;
}
}
return sfPojo;
}
}
Method 2: you can use the JsonParser to operate the JSON string, but this method is more complicated, you can ref this article:
Your model:
public class Ifce {
public String port;
public String reservableBW;
public List<String> capabilites = null;
}
public class Device {
public String device;
public List<Ifce> ifces = null;
}
Then use the objectMapper like this:
objectMapper = new ObjectMapper()
Device device = objectMapper.readValue(yourJsonData, Device.class)
List<Ifce> deviceList = device.ifces.find { it.capabilites.contains("ETHERNET")}
Related
I am trying to include raw JSON inside a Java object when the object is (de)serialized using Jackson. In order to test this functionality, I wrote the following test:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
#Test
public void test() throws JsonGenerationException, JsonMappingException, IOException {
String foo = "one";
String bar = "{\"A\":false}";
Pojo pojo = new Pojo();
pojo.foo = foo;
pojo.bar = bar;
String json = "{\"foo\":\"" + foo + "\",\"bar\":" + bar + "}";
ObjectMapper objectMapper = new ObjectMapper();
String output = objectMapper.writeValueAsString(pojo);
System.out.println(output);
assertEquals(json, output);
Pojo deserialized = objectMapper.readValue(output, Pojo.class);
assertEquals(foo, deserialized.foo);
assertEquals(bar, deserialized.bar);
}
The code outputs the following line:
{"foo":"one","bar":{"A":false}}
The JSON is exactly how I want things to look. Unfortunately, the code fails with an exception when attempting to read the JSON back in to the object. Here is the exception:
org.codehaus.jackson.map.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: java.io.StringReader#d70d7a; line: 1, column: 13] (through reference chain: com.tnal.prism.cobalt.gather.testing.Pojo["bar"])
Why does Jackson function just fine in one direction but fail when going the other direction? It seems like it should be able to take its own output as input again. I know what I'm trying to do is unorthodox (the general advice is to create an inner object for bar that has a property named A), but I don't want to interact with this JSON at all. My code is acting as a pass-through for this code -- I want to take in this JSON and send it back out again without touching a thing, because when the JSON changes I don't want my code to need modifications.
Thanks for the advice.
EDIT: Made Pojo a static class, which was causing a different error.
#JsonRawValue is intended for serialization-side only, since the reverse direction is a bit trickier to handle. In effect it was added to allow injecting pre-encoded content.
I guess it would be possible to add support for reverse, although that would be quite awkward: content will have to be parsed, and then re-written back to "raw" form, which may or may not be the same (since character quoting may differ).
This for general case. But perhaps it would make sense for some subset of problems.
But I think a work-around for your specific case would be to specify type as 'java.lang.Object', since this should work ok: for serialization, String will be output as is, and for deserialization, it will be deserialized as a Map. Actually you might want to have separate getter/setter if so; getter would return String for serialization (and needs #JsonRawValue); and setter would take either Map or Object. You could re-encode it to a String if that makes sense.
Following #StaxMan answer, I've made the following works like a charm:
public class Pojo {
Object json;
#JsonRawValue
public String getJson() {
// default raw value: null or "[]"
return json == null ? null : json.toString();
}
public void setJson(JsonNode node) {
this.json = node;
}
}
And, to be faithful to the initial question, here is the working test:
public class PojoTest {
ObjectMapper mapper = new ObjectMapper();
#Test
public void test() throws IOException {
Pojo pojo = new Pojo("{\"foo\":18}");
String output = mapper.writeValueAsString(pojo);
assertThat(output).isEqualTo("{\"json\":{\"foo\":18}}");
Pojo deserialized = mapper.readValue(output, Pojo.class);
assertThat(deserialized.json.toString()).isEqualTo("{\"foo\":18}");
// deserialized.json == {"foo":18}
}
}
I was able to do this with a custom deserializer (cut and pasted from here)
package etc;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
/**
* Keeps json value as json, does not try to deserialize it
* #author roytruelove
*
*/
public class KeepAsJsonDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
TreeNode tree = jp.getCodec().readTree(jp);
return tree.toString();
}
}
Use it by annotating the desired member like this:
#JsonDeserialize(using = KeepAsJsonDeserializer.class)
private String value;
#JsonSetter may help. See my sample ('data' is supposed to contain unparsed JSON):
class Purchase
{
String data;
#JsonProperty("signature")
String signature;
#JsonSetter("data")
void setData(JsonNode data)
{
this.data = data.toString();
}
}
This is a problem with your inner classes. The Pojo class is a non-static inner class of your test class, and Jackson cannot instantiate that class. So it can serialize, but not deserialize.
Redefine your class like this:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
Note the addition of static
Adding to Roy Truelove's great answer, this is how to inject the custom deserialiser in response to appearance of #JsonRawValue:
import com.fasterxml.jackson.databind.Module;
#Component
public class ModuleImpl extends Module {
#Override
public void setupModule(SetupContext context) {
context.addBeanDeserializerModifier(new BeanDeserializerModifierImpl());
}
}
import java.util.Iterator;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
public class BeanDeserializerModifierImpl extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> it = builder.getProperties();
while (it.hasNext()) {
SettableBeanProperty p = it.next();
if (p.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(p.withValueDeserializer(KeepAsJsonDeserialzier.INSTANCE), true);
}
}
return builder;
}
}
This easy solution worked for me:
public class MyObject {
private Object rawJsonValue;
public Object getRawJsonValue() {
return rawJsonValue;
}
public void setRawJsonValue(Object rawJsonValue) {
this.rawJsonValue = rawJsonValue;
}
}
So I was able to store raw value of JSON in rawJsonValue variable and then it was no problem to deserialize it (as object) with other fields back to JSON and send via my REST. Using #JsonRawValue didnt helped me because stored JSON was deserialized as String, not as object, and that was not what I wanted.
This even works in a JPA entity:
private String json;
#JsonRawValue
public String getJson() {
return json;
}
public void setJson(final String json) {
this.json = json;
}
#JsonProperty(value = "json")
public void setJsonRaw(JsonNode jsonNode) {
// this leads to non-standard json, see discussion:
// setJson(jsonNode.toString());
StringWriter stringWriter = new StringWriter();
ObjectMapper objectMapper = new ObjectMapper();
JsonGenerator generator =
new JsonFactory(objectMapper).createGenerator(stringWriter);
generator.writeTree(n);
setJson(stringWriter.toString());
}
Ideally the ObjectMapper and even JsonFactory are from the context and are configured so as to handle your JSON correctly (standard or with non-standard values like 'Infinity' floats for example).
Here is a full working example of how to use Jackson modules to make #JsonRawValue work both ways (serialization and deserialization):
public class JsonRawValueDeserializerModule extends SimpleModule {
public JsonRawValueDeserializerModule() {
setDeserializerModifier(new JsonRawValueDeserializerModifier());
}
private static class JsonRawValueDeserializerModifier extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
builder.getProperties().forEachRemaining(property -> {
if (property.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(property.withValueDeserializer(JsonRawValueDeserializer.INSTANCE), true);
}
});
return builder;
}
}
private static class JsonRawValueDeserializer extends JsonDeserializer<String> {
private static final JsonDeserializer<String> INSTANCE = new JsonRawValueDeserializer();
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return p.readValueAsTree().toString();
}
}
}
Then you can register the module after creating the ObjectMapper:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());
String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
I had the exact same issue.
I found the solution in this post :
Parse JSON tree to plain class using Jackson or its alternatives
Check out the last answer.
By defining a custom setter for the property that takes a JsonNode as parameter and calls the toString method on the jsonNode to set the String property, it all works out.
Using an object works fine both ways... This method has a bit of overhead deserializing the raw value in two times.
ObjectMapper mapper = new ObjectMapper();
RawJsonValue value = new RawJsonValue();
value.setRawValue(new RawHello(){{this.data = "universe...";}});
String json = mapper.writeValueAsString(value);
System.out.println(json);
RawJsonValue result = mapper.readValue(json, RawJsonValue.class);
json = mapper.writeValueAsString(result.getRawValue());
System.out.println(json);
RawHello hello = mapper.readValue(json, RawHello.class);
System.out.println(hello.data);
RawHello.java
public class RawHello {
public String data;
}
RawJsonValue.java
public class RawJsonValue {
private Object rawValue;
public Object getRawValue() {
return rawValue;
}
public void setRawValue(Object value) {
this.rawValue = value;
}
}
I had a similar problem, but using a list with a lot of JSON itens (List<String>).
public class Errors {
private Integer status;
private List<String> jsons;
}
I managed the serialization using the #JsonRawValue annotation. But for deserialization I had to create a custom deserializer based on Roy's suggestion.
public class Errors {
private Integer status;
#JsonRawValue
#JsonDeserialize(using = JsonListPassThroughDeserialzier.class)
private List<String> jsons;
}
Below you can see my "List" deserializer.
public class JsonListPassThroughDeserializer extends JsonDeserializer<List<String>> {
#Override
public List<String> deserialize(JsonParser jp, DeserializationContext cxt) throws IOException, JsonProcessingException {
if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
final List<String> list = new ArrayList<>();
while (jp.nextToken() != JsonToken.END_ARRAY) {
list.add(jp.getCodec().readTree(jp).toString());
}
return list;
}
throw cxt.instantiationException(List.class, "Expected Json list");
}
}
Im using jacson to parse the following JSON array
[
{
"target": "something",
"datapoints": [
[
null,
1482223380
]]}]
Into this POJO
public class Response {
private String target;
private List<List<Double>> datapoints;
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public List<List<Double>> getData() {
return datapoints;
}
public void setData(List<List<Double>> data) {
this.datapoints = data;
}
}
Using the following code
objectMapper.readValue(json, new TypeReference<List<Response>>() {});
This works partially, the outer list and the target is correct, however datapoints is null.
My initial solution is taken from this answere.
My question is, why are not datapoints not parsed as expected? Do this has something todo with the null values inside the array?
You could write a custom JsonDeserializer for the datapoints field.
class MyDatapointsDeserializer extends JsonDeserializer<List<List<Double>>> {
private static final TypeReference<List<List<Double>>> TYPE_REF =
new TypeReference<List<List<Double>>>() {};
#Override
public List<List<Double>> deserialize(
JsonParser jp, DeserializationContext ctxt) throws IOException {
return jp.readValueAs(TYPE_REF);
}
}
Then annotate the field accordingly.
#JsonDeserialize(using = MyDatapointsDeserializer.class)
private List<List<Double>> datapoints;
We have a service which currently consumes JSON. We want to slightly restructure this JSON (move one property one level up) but also implement graceful migration so that our service could process old structure as well as new structure. We're using Jackson for JSON deserialization.
How do we restructure JSON prior to deserialization with Jackson?
Here's a MCVE.
Assume our old JSON looks as follows:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16], "serviceId" : "0815"}}
We want to move serviceId one level up:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16]}, "serviceId" : "0815"}
This are the classes we want to deserialize from both old an new JSONs:
public final static class Container {
public final Reference reference;
public final String serviceId;
#JsonCreator
public Container(#JsonProperty("reference") Reference reference, #JsonProperty("serviceId") String serviceId) {
this.reference = reference;
this.serviceId = serviceId;
}
}
public final static class Reference {
public final String number;
public final LocalDate startDate;
#JsonCreator
public Reference(#JsonProperty("number") String number, #JsonProperty("startDate") LocalDate startDate) {
this.number = number;
this.startDate = startDate;
}
}
We only want serviceId in Container, not in both classes.
What I've got working is the following deserializer:
public static class ServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final ObjectMapper objectMapper;
{
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
#Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return objectMapper.treeToValue(node, Container.class);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
This deserializer first retrieves the tree, manipulates it and then deserializers it using an own instance of ObjectMapper. It works but we really dislike the fact that we have another instance of ObjectMapper here. If we don't create it and somehow use the system-wide instance of ObjectMapper we get an infinite cycle because when we try to call objectMapper.treeToValue, our deserializer gets called recursively. So this works (with an own instance of ObjectMapper) but it is not an optimal solution.
Another method I've tried was using a BeanDeserializerModifier and a own JsonDeserializer which "wraps" the default serializer:
public static class ServiceIdMigrationBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> defaultDeserializer) {
if (beanDesc.getBeanClass() == Container.class) {
return new ModifiedServiceIdMigratingContainerDeserializer((JsonDeserializer<Container>) defaultDeserializer);
} else {
return defaultDeserializer;
}
}
}
public static class ModifiedServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final JsonDeserializer<Container> defaultDeserializer;
public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<Container> defaultDeserializer) {
this.defaultDeserializer = defaultDeserializer;
}
#Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return defaultDeserializer.deserialize(new TreeTraversingParser(node, p.getCodec()), ctxt);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
"Wrapping" a default deserializer seems to be a better approach, but this fails with an NPE:
java.lang.NullPointerException
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:157)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:150)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:235)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:1)
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1623)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1217)
at ...
The whole MCVE code is in the following PasteBin. It is a single-class all-containing test case which demonstrates both approaches. The migratesViaDeserializerModifierAndUnmarshalsServiceId fails.
So this leaves me with a question:
How do we restructure JSON prior to deserialization with Jackson?
In the best traditions, right after posting the question, I've managed to solve this.
Two things:
I had to do newJsonParser.nextToken(); to avoid NPE.
Extend DelegatingDeserializer
Here's a working DelegatingDeserializer:
public static class ModifiedServiceIdMigratingContainerDeserializer
extends DelegatingDeserializer {
public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<?> defaultDeserializer) {
super(defaultDeserializer);
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new ModifiedServiceIdMigratingContainerDeserializer(newDelegatee);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return super.deserialize(restructure(p), ctxt);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException,
JsonProcessingException {
return super.deserialize(restructure(p), ctxt, intoValue);
}
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException, JsonProcessingException {
return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);
}
public JsonParser restructure(JsonParser p) throws IOException, JsonParseException {
final ObjectNode node = p.readValueAsTree();
migrate(node);
final TreeTraversingParser newJsonParser = new TreeTraversingParser(node, p.getCodec());
newJsonParser.nextToken();
return newJsonParser;
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
I have problem with modelling server responses, some of them look like that:
{
"_links":{
"self":{
"href":"http:\/\/example.com"
}
},
"_embedded":{
"category":{
<...data...>
}
}
}
or
{
"_links":{
"self":{
"href":"http:\/\/example.com"
}
},
"_embedded":{
"episodes":[
<...list_data...>
]
}
}
It seems that "_embedded" property has only one JSON object and that object has only one property ( named differently ) with actual data.
I would like to create some kind of generic POJO class to support those kind of responses, something like:
public abstract class EmbeddedResponse<T> {
#JsonProperty("_embedded")
private T embedded;
public T getEmbedded() {
return embedded;
}
... <other_members> ...
}
public class CategoriesResponse extends EmbeddedResponse<List<Category>> {
}
Where calling 'getEmbedded()' would return list of categories ( or episodes, or anything ).
I am working with custom deserialization now, but without much success, I would like to keep code base minimal.
Solution, abstract POJO class:
public class EmbeddedResponse<T> {
#JsonProperty("_embedded")
#JsonDeserialize( using = EmbeddedResponseDeserializer.class )
private T embedded;
public T getEmbedded() {
return embedded;
}
}
POJO for actual response:
public class CategoriesResponse extends EmbeddedResponse<List<Category>> {
}
Deserializer for JSON in question:
public class EmbeddedResponseDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer {
private JavaType javaType;
#Override
public Object deserialize( JsonParser jsonParser, DeserializationContext ctxt ) throws IOException {
ObjectCodec objectCodec = jsonParser.getCodec();
JsonNode node = objectCodec.readTree(jsonParser);
// Get first it might require correction
String fieldName = node.fieldNames().next();
JsonNode skippedNode = node.get( fieldName );
return objectCodec.readValue( skippedNode.traverse(), javaType );
}
#Override
public JsonDeserializer<?> createContextual( DeserializationContext ctxt, BeanProperty property ) throws JsonMappingException {
javaType = property.getType();
return this;
}
}
It might require more tweeks but at this point this solution is working
I would use the Java 8 Optional object when modelling the objects. This way you get a flexible model and nice programming model by e.g. using the ifPresent-method.
So, the root class could be modelled along the lines of:
public class Response {
private Embedded embedded;
private Links links;
#JsonCreator
public Response(
#JsonProperty("_links") final Links links,
#JsonProperty("_embedded") final Embedded embedded) {
this.links = links;
this.embedded = embedded;
}
public Embedded embedded() {
return embedded;
}
public Links links() {
return links;
}
}
The object that defines the embedded content (i.e. category or episodes) could be modelled like this:
public class Embedded {
private final Category category;
private final List<Episode> episodes;
#JsonCreator
public Embedded(
#JsonProperty("episodes") final List<Episode> episodes,
#JsonProperty("category") final Category category) {
this.episodes = episodes;
this.category = category;
}
public Optional<Category> category() {
return Optional.ofNullable(category);
}
public Optional<List<Episode>> episodes() {
return Optional.ofNullable(episodes);
}
}
When programming towards these objects the following pattern could be used:
final InputStream resource = ...; // retrieve a stream somehow
// Map the stream to the response object
final Response response = new ObjectMapper().readValue(resource, Response.class);
// Use the Optional-style for processing category data
response.embedded().category().ifPresent(category -> {
// do category stuff with the Category-object
});
// Once more, use the Optional-style - this time for processing episodes data
response.embedded().episodes().ifPresent(episodes -> {
// do episodes stuff with the List of Episodes
});
I am using Jackson fasterxml for unmarshalling JSON. In my object there are two kinds of properties:Input properties and Calculated properties. In the input JSON, I get only input values.
The calculated values are actually dependent on input values. I have to populate these values before the object gets referred. So I am just checking if there are any hooks provided by Jackson so that I can do my calculations there. For example JAXB provides afterUnmarshal method to customize the unmarshaling behavior:
void afterUnmarshal(Unmarshaller u, Object parent)
But I could not find similar information about customizing Jackson. Are any such framework hooks provided by Jackson to customize the unmarshaling behavior?
I'd rather recommend to keep your model objects immutable by using constructor creators. That is, all the JSON values are passed to a constructor which would initialize the other calculated properties.
Anyway, if you want to customize an object after deserialization (without writing a deserializer for every type) you can modify the deserializer in a way that at the end it calls a special method(s) of a newly constructed instance. Here is an example which would work for all the classes that implements a special interface (one can consider using an annotation to mark the post construct methods).
public class JacksonPostConstruct {
public static interface PostConstructor {
void postConstruct();
}
public static class Bean implements PostConstructor {
private final String field;
#JsonCreator
public Bean(#JsonProperty("field") String field) {
this.field = field;
}
public void postConstruct() {
System.out.println("Post construct: " + toString());
}
#Override
public String toString() {
return "Bean{" +
"field='" + field + '\'' +
'}';
}
}
private static class PostConstructDeserializer extends DelegatingDeserializer {
private final JsonDeserializer<?> deserializer;
public PostConstructDeserializer(JsonDeserializer<?> deserializer) {
super(deserializer);
this.deserializer = deserializer;
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return deserializer;
}
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Object result = _delegatee.deserialize(jp, ctxt);
if (result instanceof PostConstructor) {
((PostConstructor) result).postConstruct();
}
return result;
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
return new PostConstructDeserializer(deserializer);
}
});
mapper.registerModule(module);
String json = "{\"field\":\"value\"}";
System.out.println(mapper.readValue(json, Bean.class));
}
}
Output:
Post construct: Bean{field='value'}
Bean{field='value'}
Let's assume that your JSON looks like this:
{
"input1" : "Input value",
"input2" : 3
}
And your POJO class looks like this:
class Entity {
private String input1;
private int input2;
private String calculated1;
private long calculated2;
...
}
In this case you can write a custom deserializer for your Entity class:
class EntityJsonDeserializer extends JsonDeserializer<Entity> {
#Override
public Entity deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
InnerEntity innerEntity = jp.readValueAs(InnerEntity.class);
Entity entity = new Entity();
entity.setInput1(innerEntity.input1);
entity.setInput2(innerEntity.input2);
entity.recalculate();
return entity;
}
public static class InnerEntity {
public String input1;
public int input2;
}
}
In above class you can see that Entity has a recalculate method. It could look like this:
public void recalculate() {
calculated1 = input1 + input2;
calculated2 = input1.length() + input2;
}
You can also move this logic to your deserializer class.
Now, you have to inform Jackson that you want to use your custom deserializer:
#JsonDeserialize(using = EntityJsonDeserializer.class)
class Entity {
...
}
The example below shows how to use these classes:
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(json, Entity.class));
This program prints:
Entity [input1=Input value, input2=3, calculated1=Input value3, calculated2=14]