Need to deserialize a JSON structure like below using Jackson, this is a response from a REST API and I am using Spring RestTemplate with MappingJackson2HttpMessageConverter to parse it.
Problems:
1. All elements can have one or more child elements of its own type (e.g. root has children : childABC & childAnyRandomName)
2. Child nodes do not have a standard naming convention, names can be any random string.
{
"FooBar": {
"root": {
"name": "test1",
"value": "900",
"childABC": {
"name": "test2",
"value": "700",
"childXYZ": {
"name": "test3",
"value": "600"
}
},
"childAnyRandomName": {
"name": "test4",
"value": "300"
}
}
}
}
As per my understanding, a POJO to store this could be something like this:
import java.util.Map;
public class TestObject {
private String name;
private String value;
private Map<String, TestObject> children;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Map<String, TestObject> getChildren() {
return children;
}
public void setChildren(Map<String, TestObject> children) {
this.children = children;
}
}
I have no control over the actual JSON that I am parsing so If Jackson does not work, I will have to push all of it in JsonNode and build this object structure with custom logic.
Disclaimer: I am relatively new to Jackson, so please don't mind if this was a classic text book solution that I didn't read, just point me to the documentation location.
In your bean, have the attributes you know are there:
private String blah;
#JsonProperty("blah")
#JsonSerialize(include = Inclusion.NON_NULL)
public String getBlah() {
return blah;
}
#JsonProperty("blah")
public void setBlah(String blah) {
this.blah = blah;
}
Then, add the following, which is kinda like a catch-all for any properties which you haven't explicitly mapped that appear on the JSON:
private final Map<String, JsonNode> properties = new HashMap<String, JsonNode>();
#JsonAnyGetter
public Map<String, JsonNode> getProperties() {
return properties;
}
#JsonAnySetter
public void setProperty(String key, JsonNode value) {
properties.put(key, value);
}
Alternatively, just map known complex objects to Map fields (IIRC, complex objects under that will also end up in maps).
Related
I'm coding an Spring-boot service and I'm using jackson ObjectMapper in order to handle with my jsons.
I need to split a json like this:
{
"copy": {
"mode": "mode",
"version": "version"
},
"known": "string value",
"unknown": {
"field1": "sdf",
"field2": "sdfdf"
},
"unknown2": "sdfdf"
}
I mean, my bean is like this:
public class MyBean {
private CopyMetadata copy;
private String known;
private Object others;
}
I'd like to populate known fields to MyBean properties, and move the other unknown properties inside MyBean.others property.
Known properties are which are placed as a field inside MyBean.
Any ideas?
A possible solution to this problem is to use the jackson annotations #JsonAnyGetter and #JsonAnySetter
Your Model Mybean.class should look something like this and it should work
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
public class MyBean {
private CopyMetadata copy;
private String known;
private Map<String, Object> others = new HashMap<>();
public CopyMetadata getCopy() {
return copy;
}
public void setCopy(CopyMetadata copy) {
this.copy = copy;
}
public String getKnown() {
return known;
}
public void setKnown(String known) {
this.known = known;
}
public Map<String, Object> getOthers() {
return others;
}
public void setOthers(Map<String, Object> others) {
this.others = others;
}
#JsonAnyGetter
public Map<String, Object> getUnknownFields() {
return others;
}
#JsonAnySetter
public void setUnknownFields(String name, Object value) {
others.put(name, value);
}
}
I want to use Jackson to deserialise my JSON, from Jira, into a set of POJOs. I have most of what I want working beautifully, now I just have to decode the custom field values.
My input JSON looks like:
{
"expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
"id": "104144",
"self": "https://jira.internal.net/rest/api/2/issue/104144",
"key": "PRJ-524",
"fields": {
"summary": "Redo unit tests to load from existing project",
"components": [],
"customfield_10240": {
"self": "https://jira.internal.net/rest/api/2/customFieldOption/10158",
"value": "Normal",
"id": "10158"
}
}
I can trivially load the summary and components, since I know ahead of time what the name of those JSON elements are, and can define them in my POJO:
#JsonIgnoreProperties({ "expand", "self", "id", })
public class JiraJson
{
private JiraFields fields;
private String key;
public JiraFields getFields()
{
return fields;
}
public String getKey()
{
return key;
}
public void setFields(JiraFields newFields)
{
fields = newFields;
}
public void setKey(String newKey)
{
key = newKey;
}
}
And similarly for JiraFields:
#JsonIgnoreProperties({ "issuetype", "priority", "status" })
public class JiraFields
{
private List<JiraComponent> components;
private String summary;
public List<JiraComponent> getComponents()
{
return components;
}
public String getSummary()
{
return summary;
}
public void setComponents(List<JiraComponent> newComponents)
{
components = newComponents;
}
public void setSummary(String newSummary)
{
summary = newSummary;
}
}
However, the field custom_10240 actually differs depending on which Jira system this is run against, on one it is custom_10240, on another it is custom_10345, so I cannot hard-code this into the POJO. Using another call, it is possible to know at runtime, before the deserialisation starts, what the name of the field is, but this is not possible at compile time.
Assuming that I want to map the value field into a String on JiraFields called Importance, how do I go about doing that? Or perhaps simpler, how to map this Importance onto a JiraCustomField class?
You can use a method annotated with #JsonAnySetter that accepts all properties that are undefined (and not ignored). in case of a Json Object (like the custom field in the question) Jackson passes a Map that contains all the Object properties (it may even contain Map values in case of nested objects). You can now at run time extract whatever properties you want:
#JsonIgnoreProperties({ "issuetype", "priority", "status" })
public class JiraFields
{
private List<JiraComponent> components;
private String summary;
private String importance;
// getter/setter omitted for brevity
#JsonAnySetter
public void setCustomField(String name, Object value) {
System.out.println(name); // will print "customfield_10240"
if (value instanceof Map) { // just to make sure we got a Json Object
Map<String, Object> customfieldMap = (Map<String, Object>)value;
if (customfieldMap.containsKey("value")) { // check if object contains "value" property
setImportance(customfieldMap.get("value").toString());
}
}
}
}
After searching further, I finally found the JsonAlias annotation. This is still defined at compile time, but I had something that I could search further on!
Further searching, and I found PropertyNamingStrategy, which allows you to rename what JSON field name is expected for a setter/field. This has the advantage in that this is done via a method, and the class can be constructed at runtime.
Here is the class that I used to perform this mapping:
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
public final class CustomFieldNamingStrategy
extends PropertyNamingStrategy
{
private static final long serialVersionUID = 8263960285216239177L;
private final Map<String, String> fieldRemapping;
private final Map<String, String> reverseRemapping;
public CustomFieldNamingStrategy(Map<String, String> newFieldRemappings)
{
fieldRemapping = newFieldRemappings;
reverseRemapping = fieldRemapping.entrySet()//
.stream()//
.collect(Collectors.toMap(Map.Entry::getValue,
Map.Entry::getKey));
}
#Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName)
{
if (field.getDeclaringClass().getName().equals(JiraFields.class.getName()))
{
return reverseRemapping.getOrDefault(defaultName, defaultName);
}
return defaultName;
}
#Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method,
String defaultName)
{
if (method.getDeclaringClass().getName().equals(JiraFields.class.getName()))
{
return reverseRemapping.getOrDefault(defaultName, defaultName);
}
return defaultName;
}
#Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method,
String defaultName)
{
if (method.getDeclaringClass().getName().equals(JiraFields.class.getName()))
{
return reverseRemapping.getOrDefault(defaultName, defaultName);
}
return defaultName;
}
}
{
"batchcomplete": "",
"warnings": {
"main": {
"*": "Unrecognized parameter: rvprop."
},
"extracts": {
"*": "\"exlimit\" was too large for a whole article extracts request, lowered to 1."
}
},
"query": {
"normalized": [
{
"from": "pune",
"to": "Pune"
}
],
"pages": {
"164634": {
"pageid": 164634,
"ns": 0,
"title": "Pune",
"extract": ""
}
}
}
}
In the above json the numeric key inside "pages" object is dynamic. So how do i make a pojo for this json.
Please, please, please help me.
I've searched a lot for this, but go nothing that works.
Also i'm a beginner in retrofit, so please answer in detail.
I've seen some answers which mention use of map for such cases (eg. Parse Dynamic Key Json String using Retrofit). But those answers are not elaborated properly. please help me understand it thoroughly.
You can use A hashmap for additional properties/ dynamic properties in your Pages class
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
#JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
#JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
public class Query {
private List<Address> normalized ;
private Map<String, Pages> pages;
//getter & Setter
public Map<String, Pages> getPages() {
return pages;
}
public void setPages(Map<String, Pages> pages) {
this.pages = pages;
}
public class Pages {
private String pageid;
private int ns;
private String title;
private String extract;
//define all getter and setter methods
}
public class Address{
private String from;
private String to;
}
}
//add data in Pages class by using setter methods. Page pageObj = new Page(); pageObj.setPageid("164634"); pageObj.setns(0); ...
Create local Map Map pagesObj = new HashMap<>();
pagesObj.put("164634", pageObj);
use setPages(pagesObj); to set value
Consider the following json, getting from an public API:
anyObject : {
attributes: [
{
"name":"anyName",
"value":"anyValue"
},
{
"name":"anyName",
"value":
{
"key":"anyKey",
"label":"anyLabel"
}
}
]
}
As you can see, sometimes the value is a simple string and sometimes its an object. Is it somehow possible to deserialize those kind of json-results, to something like:
class AnyObject {
List<Attribute> attributes;
}
class Attribute {
private String key;
private String label;
}
How would I design my model to cover both cases. Is that possible ?
Despite being hard to manage as others have pointed out, you can do what you want. Add a custom deserializer to handle this situation. I rewrote your beans because I felt your Attribute class was a bit misleading. The AttributeEntry class in the object that is an entry in that "attributes" list. The ValueObject is the class that represents that "key"/"label" object. Those beans are below, but here's the custom deserializer. The idea is to check the type in the JSON, and instantiate the appropriate AttributeEntry based on its "value" type.
public class AttributeDeserializer extends JsonDeserializer<AttributeEntry> {
#Override
public AttributeEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
String name = root.get("name").asText();
if (root.get("value").isObject()) {
// use your object mapper here, this is just an example
ValueObject attribute = new ObjectMapper().readValue(root.get("value").asText(), ValueObject.class);
return new AttributeEntry(name, attribute);
} else if (root.get("value").isTextual()) {
String stringValue = root.get("value").asText();
return new AttributeEntry(name, stringValue);
} else {
return null; // or whatever
}
}
}
Because of this ambiguous type inconvenience, you will have to do some type checking throughout your code base.
You can then add this custom deserializer to your object mapper like so:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(AttributeEntry.class, new AttributeDeserializer());
objectMapper.registerModule(simpleModule);
Here's the AttributeEntry:
public class AttributeEntry {
private String name;
private Object value;
public AttributeEntry(String name, String value) {
this.name = name;
this.value = value;
}
public AttributeEntry(String name, ValueObject attributes) {
this.name = name;
this.value = attributes;
}
/* getters/setters */
}
Here's the ValueObject:
public class ValueObject {
private String key;
private String label;
/* getters/setters */
}
I have a JSON array like as shown below which I need to serialize it to my class. I am using Jackson in my project.
[
{
"clientId": "111",
"clientName": "mask",
"clientKey": "abc1",
"clientValue": {}
},
{
"clientId": "111",
"clientName": "mask",
"clientKey": "abc2",
"clientValue": {}
}
]
In above JSON array, clientValue will have another JSON object in it. How can I serialize my above JSON array into my java class using Jackson?
public class DataRequest {
#JsonProperty("clientId")
private String clientId;
#JsonProperty("clientName")
private int clientName;
#JsonProperty("clientKey")
private String clientKey;
#JsonProperty("clientValue")
private Map<String, Object> clientValue;
//getters and setters
}
I have not used jackson before so I am not sure how can I use it to serialize my JSON array into Java objects? I am using jackson annotation here to serialize stuff but not sure what will be my next step?
You can create a utility function shown below. You may want to change the Deserialization feature based on your business needs. In my case, I did not want to fail on unknown properties => (FAIL_ON_UNKNOWN_PROPERTIES, false)
static <T> T mapJson(String body,
com.fasterxml.jackson.core.type.TypeReference<T> reference) {
T model = null;
if(body == null) {
return model;
}
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
try {
model = mapper.readValue(body, reference);
} catch (IOException e) {
//TODO: log error and handle accordingly
}
return model;
}
You can call it using similar approach as shown below:
mapJson(clientValueJsonString,
new com.fasterxml.jackson.core.type.TypeReference<List<DataRequest>>(){});
You can try #JsonAnyGetter and #JsonAnySetter annotations with an inner class object. Also clientName should have type String, not int.
public class DataRequest {
private String clientId;
private String clientName;
private String clientKey;
private ClientValue clientValue;
//getters and setters
}
public class ClientValue {
private Map<String, String> properties;
#JsonAnySetter
public void add(String key, String value) {
properties.put(key, value);
}
#JsonAnyGetter
public Map<String,String> getProperties() {
return properties;
}
}