Here is a rough but more declarative solution. I haven't been able to get it down to a single annotation, but this seems to work well. Also not sure about performance on large data sets.
Given this JSON:
{
"list": [
{
"wrapper": {
"name": "Jack"
}
},
{
"wrapper": {
"name": "Jane"
}
}
]
}
And these model objects:
public class RootObject {
@JsonProperty("list")
@JsonDeserialize(contentUsing = SkipWrapperObjectDeserializer.class)
@SkipWrapperObject("wrapper")
public InnerObject[] innerObjects;
}
and
public class InnerObject {
@JsonProperty("name")
public String name;
}
Where the Jackson voodoo is implemented like:
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface SkipWrapperObject {
String value();
}
and
public class SkipWrapperObjectDeserializer extends JsonDeserializer<Object> implements
ContextualDeserializer {
private Class<?> wrappedType;
private String wrapperKey;
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException {
SkipWrapperObject skipWrapperObject = property
.getAnnotation(SkipWrapperObject.class);
wrapperKey = skipWrapperObject.value();
JavaType collectionType = property.getType();
JavaType collectedType = collectionType.containedType(0);
wrappedType = collectedType.getRawClass();
return this;
}
@Override
public Object deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode objectNode = mapper.readTree(parser);
JsonNode wrapped = objectNode.get(wrapperKey);
Object mapped = mapIntoObject(wrapped);
return mapped;
}
private Object mapIntoObject(JsonNode node) throws IOException,
JsonProcessingException {
JsonParser parser = node.traverse();
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(parser, wrappedType);
}
}
Hope this is useful to someone!
Answer from Patrick on Stack OverflowHere is a rough but more declarative solution. I haven't been able to get it down to a single annotation, but this seems to work well. Also not sure about performance on large data sets.
Given this JSON:
{
"list": [
{
"wrapper": {
"name": "Jack"
}
},
{
"wrapper": {
"name": "Jane"
}
}
]
}
And these model objects:
public class RootObject {
@JsonProperty("list")
@JsonDeserialize(contentUsing = SkipWrapperObjectDeserializer.class)
@SkipWrapperObject("wrapper")
public InnerObject[] innerObjects;
}
and
public class InnerObject {
@JsonProperty("name")
public String name;
}
Where the Jackson voodoo is implemented like:
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface SkipWrapperObject {
String value();
}
and
public class SkipWrapperObjectDeserializer extends JsonDeserializer<Object> implements
ContextualDeserializer {
private Class<?> wrappedType;
private String wrapperKey;
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException {
SkipWrapperObject skipWrapperObject = property
.getAnnotation(SkipWrapperObject.class);
wrapperKey = skipWrapperObject.value();
JavaType collectionType = property.getType();
JavaType collectedType = collectionType.containedType(0);
wrappedType = collectedType.getRawClass();
return this;
}
@Override
public Object deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode objectNode = mapper.readTree(parser);
JsonNode wrapped = objectNode.get(wrapperKey);
Object mapped = mapIntoObject(wrapped);
return mapped;
}
private Object mapIntoObject(JsonNode node) throws IOException,
JsonProcessingException {
JsonParser parser = node.traverse();
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(parser, wrappedType);
}
}
Hope this is useful to someone!
Your data is problematic in that you have inner wrapper objects in your array. Presumably your Vendor object is designed to handle id, name, company_id, but each of those multiple objects are also wrapped in an object with a single property vendor.
I'm assuming that you're using the Jackson Data Binding model.
If so then there are two things to consider:
The first is using a special Jackson config property. Jackson - since 1.9 I believe, this may not be available if you're using an old version of Jackson - provides UNWRAP_ROOT_VALUE. It's designed for cases where your results are wrapped in a top-level single-property object that you want to discard.
So, play around with:
objectMapper.configure(SerializationConfig.Feature.UNWRAP_ROOT_VALUE, true);
The second is using wrapper objects. Even after discarding the outer wrapper object you still have the problem of your Vendor objects being wrapped in a single-property object. Use a wrapper to get around this:
class VendorWrapper
{
Vendor vendor;
// gettors, settors for vendor if you need them
}
Similarly, instead of using UNWRAP_ROOT_VALUES, you could also define a wrapper class to handle the outer object. Assuming that you have correct Vendor, VendorWrapper object, you can define:
class VendorsWrapper
{
List<VendorWrapper> vendors = new ArrayList<VendorWrapper>();
// gettors, settors for vendors if you need them
}
// in your deserialization code:
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readValue(jsonInput, VendorsWrapper.class);
The object tree for VendorsWrapper is analogous to your JSON:
VendorsWrapper:
vendors:
[
VendorWrapper
vendor: Vendor,
VendorWrapper:
vendor: Vendor,
...
]
Finally, you might use the Jackson Tree Model to parse this into JsonNodes, discarding the outer node, and for each JsonNode in the ArrayNode, calling:
mapper.readValue(node.get("vendor").getTextValue(), Vendor.class);
That might result in less code, but it seems no less clumsy than using two wrappers.
Attributes and nested de-serialization
java - Deserializing field from nested objects within JSON response with Jackson - Stack Overflow
java - How to let Jackson deserialize nested objects in custom deserializer - Stack Overflow
Conditionally Deserialize Nested JSON using Jackson
I'm using Jackson to deserialize JSON that has a nested object with a different structure depending on the endpoint I'm calling. If I wanted to deserialize the outer JSON structure to a single class/object for both endpoints, how would I conditionally deserialize to one of two classes depending on the structure of the inner JSON?
API Endpoint 1
{
"num_results": 20,
"results": [
{
"title": "Florida man sentenced to 30 days in jail",
"section": "Local"
},
// ...
]
}
API Endpoint 2
{
"num_results": 20,
"results": [
{
"section": "LOCAL",
"name": "Local News"
},
// ...
]
}The API to deserialize JSON of indeterminate structure is:
JSON.deserializeUntyped
described in the JSON Class documentation. I have used this often and have not hit any depth problems; add a link to any evidence of that.
That will return arrays and maps of Object and so your code will need to cast appropriately. This can be static - the code assumes and casts - or dynamic - the code uses instanceof (and other type detection) and bases the casting on that. Putting the mapping to SObject fields in some kind of configuration or at the very least in some static Apex maps makes sense. Note that you will probably have to implement type conversion for e.g. dates
(Perhaps not relevant here, but in general, do consider json2apex that has the benefit of generating data holding classes for you. If your JSON only has a small number of permutations you could generate per permutation and select the appropriate one via a preliminary JSON.deserializeUntyped.)
My goto reference for what can turn up in JSON is this single Introducing JSON page.
To Deserialize JSON you can use:
JSON.deserializeUntyped
To parse JSON for multiple levels using JSON.deserializeUntyped you need another layer of typecasting.
Here is an example:
JSON To Parse:
{
"Candidates":[
{
"Name":"ABC",
"Exp":25,
"Languages":[
{
"Name":"Apex",
"version":[
]
},
{
"Name":"Java",
"versions":[
{
"version":1.8,
"certification":true,
"placeholder":
{
"target":"reached"
}
}
]
}
]
},
{
"Name":"DEF",
"Exp":26
}
]
}
Let's say we need to reach the target attribute. Levels are Candidates>Languages>versions>placeholder>target.
You can achieve it by additional type casting for each level you move. Here is the solution:
String jsonInput = '{'+
'"Candidates":['+
'{'+
'"Name":"ABC",'+
'"Exp":25,'+
'"Languages":['+
'{'+
'"Name":"Apex",'+
'"versions":['+
']'+
'},'+
'{' +
'"Name":"java",'+
'"versions":['+
'{'+
'"version":1.8,'+
'"certification":true,'+
'"placeholder":'+
'{'+
'"target":"reached"'+
'}'+
''+
'}'+
']'+
'}'+
']'+
'},'+
'{'+
'"Name":"DEF",'+
'"Exp":26'+
'}'+
']'+
'}';
//Deserialize the specified JSON string into collections of primitive data types.
Map<String, Object> m =
(Map<String, Object>)
JSON.deserializeUntyped(jsonInput);
//level-1
List<Object> Candidates = (List<Object>)m.get('Candidates');
for(Object Candidate : Candidates){
String Name = (String)((Map<String, Object>) Candidate).get('Name');
if(Name == 'ABC'){
//level-2
List<Object> Languages = (List<Object>)((Map<String, Object>) Candidate).get('Languages');
for(Object Language : Languages){
String LanguageName = (String)((Map<String, Object>) Language).get('Name');
if(LanguageName == 'java'){
//level-3
List<Object> versions = (List<Object>)((Map<String, Object>) Language).get('versions');
for(Object version : versions){
Decimal versionNo = (Decimal)((Map<String, Object>) version).get('version');
if(versionNo == 1.8){
//level-4
Map<String, Object> placeholder = (Map<String, Object>)((Map<String, Object>) version).get('placeholder');
//level-5
String target = (String)placeholder.get('target');
System.debug(target);
}
}
}
}
}
}
You can write custom deserializer for List<Item> items. See below example:
class ItemsJsonDeserializer extends JsonDeserializer<List<Item>> {
@Override
public List<Item> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
InnerItems innerItems = jp.readValueAs(InnerItems.class);
return innerItems.elements;
}
private static class InnerItems {
public List<Item> elements;
}
}
Now, you have to inform Jackson to use it for your property. You can do this in this way:
public class Order {
public String name;
@JsonDeserialize(using = ItemsJsonDeserializer.class)
public List<Item> items;
}
In general it is best to map JSON structure exactly to Java. In your case you could use something like:
public class Order {
public String name;
public ItemList items;
}
public class ItemList {
public List<Item> elements;
// and any properties you might want...
}
alternatively, you could probably also use (relatively) new @JsonFormat annotation:
public class Order {
public String name;
public ItemList items;
}
// annotation can be used on propery or class
@JsonFormat(shape=Shape.OBJECT) // instead of Shape.ARRAY
public class ItemList extends ArrayList<Item>
{
public Iterator<Item> getElements() { return this.iterator(); }
public String getSomeAttribute() { ... }
}
where you are forcing List or Collection to be serialized as if it was POJO, instead of normal special handling. There may be some side-effects, since introspection is used to find possible accessors, but the general approach should work