Parsing dynamically generated JSON object names with Jackson - java

I'm attempting to deserialize some MediaWiki context from JSON using Jackson into POJOs. However, the problem is that one of the JSON object names is the integer ID value of the article, so using an annotation like #JsonProperty can't be used because the value is never constant.
Here's some sample JSON to describe what I mean:
http://en.wikipedia.org/w/api.php?action=query&titles=Albert%20Einstein&prop=info&format=json&indexpageids
{
"query": {
"pageids": [
"736"
],
"pages": {
"736": {
"pageid": 736,
"ns": 0,
"title": "Albert Einstein",
"contentmodel": "wikitext",
"pagelanguage": "en",
"touched": "2014-01-05T03:14:23Z",
"lastrevid": 588780054,
"counter": "",
"length": 106159
}
}
}
}
(MediaWiki recommends adding the &indexpageids parameter to assist with parsing, however I can't see how it would be useful to me.)
I tried using the #JsonAnyGetter and #JsonAnySetter annotations as well but they don't appear to help, throwing the same exception com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "736" (class WikipediaPages), not marked as ignorable (one known property: "wikipediaPage"]).
Thanks for any and all assistance.
Edit: Here's what the relevant classes look like at the moment:
public class WikipediaPages {
private Map<String, WikipediaPage> wikipediaPageMap = new HashMap<String, WikipediaPage>();
public Map<String, WikipediaPage> getWikipediaPageMap() {
return wikipediaPageMap;
}
public void setWikipediaPageMap(Map<String, WikipediaPage> wikipediaPageMap) {
this.wikipediaPageMap = wikipediaPageMap;
}
}
I use a Jackson Mixin to apply annotations:
public interface WikipediaPagesMixIn {
#JsonAnyGetter
Map<String, WikipediaPage> getWikipediaPageMap();
#JsonAnySetter
void setWikipediaPageMap(Map<String, WikipediaPage> wikipediaPageMap);
}
Edit 2: More code, as requested:
public class JacksonBuilder {
private static ObjectMapper objectMapper;
public static ObjectMapper getObjectMapper() {
if(objectMapper == null) {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new WikipediaModule());
}
return objectMapper;
}
}
public class WikipediaModule extends SimpleModule {
public WikipediaModule() {
super("WikipediaModule", new Version(1, 0, 0, null, "net.ryanmorrison", "sentience"));
}
#Override
public void setupModule(SetupContext setupContext) {
setupContext.setMixInAnnotations(WikipediaPage.class, WikipediaPageMixIn.class);
setupContext.setMixInAnnotations(WikipediaPages.class, WikipediaPagesMixIn.class);
setupContext.setMixInAnnotations(WikipediaQuery.class, WikipediaQueryMixIn.class);
setupContext.setMixInAnnotations(WikipediaResult.class, WikipediaResultMixIn.class);
}
}
public class WikipediaResult {
private WikipediaQuery wikipediaQuery;
public WikipediaQuery getWikipediaQuery() {
return wikipediaQuery;
}
public void setWikipediaQuery(WikipediaQuery wikipediaQuery) {
this.wikipediaQuery = wikipediaQuery;
}
}
public interface WikipediaResultMixIn {
#JsonProperty("query")
WikipediaQuery getWikipediaQuery();
}

To answer the root cause of your exception, the #JsonAnySetter javadoc states
Marker annotation that can be used to define a non-static,
two-argument method (first argument name of property, second value to
set), [...]
As such, using a mixin like this
#JsonAnySetter
void setWikipediaPageMap(Map<String, WikipediaPage> wikipediaPageMap);
doesn't register it and therefore the property isn't found.
Honestly, don't use mixins if you control the data classes. You can directly map the fields as I've shown below.
I don't know how you are using your mixin, but the following works for me
String json = "{ \"query\": { \"pageids\": [ \"736\" ], \"pages\": { \"736\": { \"pageid\": 736, \"ns\": 0, \"title\": \"Albert Einstein\", \"contentmodel\": \"wikitext\", \"pagelanguage\": \"en\", \"touched\": \"2014-01-05T03:14:23Z\", \"lastrevid\": 588780054, \"counter\": \"\", \"length\": 106159 } } } }";
ObjectMapper mapper = new ObjectMapper();
JsonNode node =mapper.readTree(json);
node = node.get("query").get("pages");
Map<String, Page> pages = mapper.readValue(node.traverse(), new TypeReference<Map<String, Page>>() {
});
System.out.println(pages);
prints
{736=Page [pageid=736, ns=0, title=Albert Einstein, contentmodel=wikitext, pagelanguage=en, touched=2014-01-05T03:14:23Z, lastrevid=588780054, counter=, length=106159]}
Where Page is
class Page {
private int pageid;
private int ns;
private String title;
private String contentmodel;
private String pagelanguage;
private String touched; // this could be a Date, with the appropriate format configuration
private int lastrevid;
private String counter;
private int length;
#Override
public String toString() {
return "Page [pageid=" + pageid + ", ns=" + ns + ", title=" + title
+ ", contentmodel=" + contentmodel + ", pagelanguage="
+ pagelanguage + ", touched=" + touched + ", lastrevid="
+ lastrevid + ", counter=" + counter + ", length=" + length
+ "]";
}
public int getPageid() {
return pageid;
}
public void setPageid(int pageid) {
this.pageid = pageid;
}
public int getNs() {
return ns;
}
public void setNs(int ns) {
this.ns = ns;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContentmodel() {
return contentmodel;
}
public void setContentmodel(String contentmodel) {
this.contentmodel = contentmodel;
}
public String getPagelanguage() {
return pagelanguage;
}
public void setPagelanguage(String pagelanguage) {
this.pagelanguage = pagelanguage;
}
public String getTouched() {
return touched;
}
public void setTouched(String touched) {
this.touched = touched;
}
public int getLastrevid() {
return lastrevid;
}
public void setLastrevid(int lastrevid) {
this.lastrevid = lastrevid;
}
public String getCounter() {
return counter;
}
public void setCounter(String counter) {
this.counter = counter;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
}
All that is left is to put the Map<String, Page> as a field in some wrapper class for the query and pages JSON elements.

Related

How to model hierarchical json in java

I'm a front end developer who is brand new to backend development. My task is to model json in a Java object. It's just some mock data for now that my controller returns.
{
"data":{
"objectId":25,
"columnName":[
"myCategory",
"myCategoryId"
],
"columnValues":[
[
"Category One",
1
],
[
"Category Two",
2
],
[
"Category Three",
3
],
[
"Category Four",
4
],
[
"Category Five",
5
]
]
}
}
And here's my attempt. The controller returns this json correctly. But isn't this too simple? What I believe should be done is extrapolate the columnName and columnValues arrays into separate classes but I'm not sure how.
package com.category;
import java.util.List;
public class MyObjectData {
private int objectId;
private List columnName;
private List columnValues;
public int getObjectId() {
return objectId;
}
public void setObjectId(int objectId) {
this.objectId = objectId;
}
public List getColumnName() {
return columnName;
}
public void setColumnName(List colName) {
this.columnName = colName;
}
public List getColumnValues() {
return columnValues;
}
public void setValues(List values) {
this.columnValues = values;
}
}
Regarding the columnNames and columnValues, I feel like I should be doing something like this in the model instead:
private List<ColumnNames> columnNames;
private List<ColumnValues> columnValues;
public List<ColumnNames> getColumnNames() {
return columnNames;
}
public void setColumnNames(List<ColumnNames> columnNames) {
this.columnNames = columnNames;
}
public List<ColumnValues> getColumnValues() {
return columnValues;
}
public void setColumnValues(List<ColumnValues> columnValues) {
this.columnValues = columnValues;
}
And then I'd have two separate classes for them like this:
package com.category;
import java.util.List;
public class ColumnName {
private String columnName;
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
}
package com.category;
import java.util.List;
public class ColumnValue {
private String columnValue;
private int columnValueId;
public String getColumnValue() {
return columnValue;
}
public void setColumnValue(String columnValue) {
this.columnValue = columnValue;
}
public String getColumnValueId() {
return columnValueId;
}
public void setColumnValueId(int columnValueId) {
this.columnValueId = columnValueId;
}
}
I feel like I have all the right pieces but just not sure if this is a better approach than my initial attempt...which works. Just looking for input. Thanks in advance.
In your structure, columnValues is actually the rows of your table that has two columns: myCategory and myCategoryId.
A more "object oriented" Java class could be something like this instead:
public class MyObjectData {
private int objectId;
private List<MyObjectRow> columnValues; // I would have named this as rows
}
public class MyObjectRow {
private String myCategory;
private String myCategoryId;
}
Now you need a custom serializer to turn this into your expected JSON structure:
public class MyObjectDataSerializer extends StdSerializer<MyObjectData> {
public MyObjectDataSerializer() {
super(MyObjectData.class);
}
public void serialize(MyObjectData value, JsonGenerator generator, SerializerProvider provider) throws IOException {
generator.writeStartObject();
generator.writeNumberField("objectId", value.getObjectId());
generator.writeArrayFieldStart("columnName");
generator.writeString("myCategory");
generator.writeString("myCategoryId");
generator.writeEndArray();
generator.writeArrayFieldStart("columnValues");
for (MyObjectRow row : value.getColumnValues()) {
generator.writeStartArray();
generator.writeString(row.getMyCategory());
generator.writeNumber(row.getMyCategoryId());
generator.writeEndArray();
}
generator.writeEndArray();
generator.writeEndObject();
}
}
Note: You can use reflection to extract the field names and values dynamically.
Then you can serialize MyObjectData objects into your expected form:
public class MyObjectDataSerializerTest {
#Test
public void shouldCustomSerializeMyObjectData() throws Exception {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(MyObjectData.class, new MyObjectDataSerializer());
mapper.registerModule(module);
MyObjectData myObjectData = new MyObjectData();
myObjectData.setObjectId(25);
myObjectData.setColumnValues(Arrays.asList(
new MyObjectRow("Category One", 1),
new MyObjectRow("Category Two", 2),
new MyObjectRow("Category Three", 3)
));
String serialized = mapper.writeValueAsString(myObjectData);
assertThat(serialized, equalTo("{\"objectId\":25,\"columnName\":[\"myCategory\",\"myCategoryId\"],\"columnValues\":[[\"Category One\",1],[\"Category Two\",2],[\"Category Three\",3]]}\n"));
}
}

Cannot deserialize instance of `org.json.JSONObject`

I have a basic SpringBoot 2.1.5.RELEASE app. Using Spring Initializer, JPA, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR file with some RestControllers.
In 1 of the controller this is the body I send:
{
"depositHotel": "xxx",
"destinationHotel": "aaa",
"depositHotelAmount": "0.2",
"destinationHotelAmount": "4",
"destinationAddress": [{
"address": "asdf",
"tag": ""
}],
"refundAddress": [{
"address": "pio",
"tag": ""
}]
}
so I create this class to use it as a RequestBody:
public class HotelswitchHotelOrderRequestBody {
public static class Builder {
private String depositHotel;
private String destinationHotel;
private Float depositHotelAmount;
private Float destinationHotelAmount;
private JSONObject destinationAddress;
private JSONObject refundAddress;
public Builder(String depositHotel, String destinationHotel) {
this.depositHotel = depositHotel;
this.destinationHotel = destinationHotel;
}
public Builder withDepositHotelAmount (Float depositHotelAmount) {
this.depositHotelAmount = depositHotelAmount;
return this;
}
public Builder withDestinationHotelAmount (Float destinationHotelAmount) {
this.destinationHotelAmount = destinationHotelAmount;
return this;
}
public Builder toDestinationAddress (JSONObject destinationAddress) {
this.destinationAddress = destinationAddress;
return this;
}
public Builder toRefundAddress (JSONObject refundAddress) {
this.refundAddress = refundAddress;
return this;
}
public HotelswitchHotelOrderRequestBody build(){
HotelswitchHotelOrderRequestBody order = new HotelswitchHotelOrderRequestBody();
order.depositHotel = this.depositHotel;
order.depositHotelAmount = this.depositHotelAmount;
order.destinationAddress = this.destinationAddress;
order.destinationHotel = this.destinationHotel;
order.destinationHotelAmount = this.destinationHotelAmount;
order.refundAddress = this.refundAddress;
return order;
}
}
private String depositHotel;
private String destinationHotel;
private Float depositHotelAmount;
private Float destinationHotelAmount;
private JSONObject destinationAddress;
private JSONObject refundAddress;
private HotelswitchHotelOrderRequestBody () {
//Constructor is now private.
}
public String getDepositHotel() {
return depositHotel;
}
public void setDepositHotel(String depositHotel) {
this.depositHotel = depositHotel;
}
public String getDestinationHotel() {
return destinationHotel;
}
public void setDestinationHotel(String destinationHotel) {
this.destinationHotel = destinationHotel;
}
public Float getDepositHotelAmount() {
return depositHotelAmount;
}
public void setDepositHotelAmount(Float depositHotelAmount) {
this.depositHotelAmount = depositHotelAmount;
}
public Float getDestinationHotelAmount() {
return destinationHotelAmount;
}
public void setDestinationHotelAmount(Float destinationHotelAmount) {
this.destinationHotelAmount = destinationHotelAmount;
}
public JSONObject getDestinationAddress() {
return destinationAddress;
}
public void setDestinationAddress(JSONObject destinationAddress) {
this.destinationAddress = destinationAddress;
}
public JSONObject getRefundAddress() {
return refundAddress;
}
public void setRefundAddress(JSONObject refundAddress) {
this.refundAddress = refundAddress;
}
}
But I have this error when receiving the object:
JSON parse error: out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `org.json.JSONObject` out of START_ARRAY token
JSONObject's representation in actual JSON is a hash i.e. {...}. In your json data you're providing an array of hashes [{...}] which is not the same. Judging from your domain I don't think it has to be multiple values, so you can just omit [] in your payload and if it does then the fields in your Java class can be defined as JSONArray.
However, I think you should go with defining an Address class and either using
private Address destinationAddress;
private Address refundAddress;
or if it indeed does have to be an object array
private List<Address> destinationAddresses;
private List<Address> refundAddresses;
I had a similar usecase , where I could not define the json to a POJO. Using com.fasterxml.jackson.databind.JsonNode instead of the JSONObject worked.

Jackson Serialization for subclasses

In the below example, I have a primary class - A and its subclass - B. Both can be used as a property in the general class X.
public class A
{
#JsonProperty("primary_key")
public final String primaryKey;
#JsonCreator
A(#JsonProperty("primary_key") String primaryKey)
{
this.primaryKey = primaryKey;
}
}
public class B extends A
{
#JsonProperty("secondary_key")
public final String secondaryKey;
#JsonCreator
B(#JsonProperty("primary_key") String primaryKey, #JsonProperty("secondary_key") String secondaryKey)
{
super(primaryKey);
this.secondaryKey = secondaryKey;
}
}
public class X
{
#JsonProperty("keys")
public final A keys;
#JsonCreator
X(#JsonProperty("keys") A keys)
{
this.keys = keys;
}
}
How can I use Jackson Polymorphic feature in order to correctly deserialize the below given json into their respective classes:
JSON A :
{ "keys" :{
"primary_key" : "abc"
}
}
JSON B :
{ "keys" : {
"primary_key" : "abc",
"secondary_key" : "xyz"
}
}
Expected Result: Map keys object to Class A for JSON A and Class B for JSON B.
Please suggest alternative suggestions too.
It feels like a pretty common problem and there is no easy annotations way to solve it (Or maybe i just cant find one):
Jackson Polymorphic Deserialization - Can you require the existence of a field instead of a specific value?
Deserializing polymorphic types with Jackson
One thing you can do is to add custom deserializer to your object mapper. Here is nice demo of this approach: https://stackoverflow.com/a/19464580/1032167
Here is demo related to your example:
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
public class Main4 {
private static final String jsonA = "{ \"keys\" : { \"primary_key\" : \"abc\" } }";
private static final String jsonB =
"{ \"keys\" : { \"primary_key\" : \"abc\", \"secondary_key\" : \"xyz\" } }";
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule idAsRefModule = new SimpleModule("ID-to-ref");
idAsRefModule.addDeserializer(A.class, new AJsonDeserializer());
mapper.registerModule(idAsRefModule);
X tl = mapper.readValue(jsonA, X.class);
System.out.println(tl);
X t2 = mapper.readValue(jsonB, X.class);
System.out.println(t2);
}
public static class AJsonDeserializer extends JsonDeserializer<A>{
#Override
public A deserialize(JsonParser jp, DeserializationContext dc)
throws IOException {
ObjectCodec codec = jp.getCodec();
JsonNode node = codec.readTree(jp);
if (node.has("secondary_key")) {
return codec.treeToValue(node, B.class);
}
return new A(node.findValue("primary_key").asText());
}
}
public static class A
{
#JsonProperty("primary_key")
public final String primaryKey;
#JsonCreator
A(#JsonProperty("primary_key") String primaryKey)
{
this.primaryKey = primaryKey;
}
#Override
public String toString() {
return "A{" +
"primaryKey='" + primaryKey + '\'' +
'}';
}
}
public static class B extends A
{
#JsonProperty("secondary_key")
public final String secondaryKey;
#JsonCreator
B(#JsonProperty("primary_key") String primaryKey,
#JsonProperty("secondary_key") String secondaryKey)
{
super(primaryKey);
this.secondaryKey = secondaryKey;
}
#Override
public String toString() {
return "B{" +
"primaryKey='" + primaryKey + '\'' +
"secondaryKey='" + secondaryKey + '\'' +
'}';
}
}
public static class X
{
#JsonProperty("keys")
public final A keys;
#JsonCreator
X(#JsonProperty("keys") A keys)
{
this.keys = keys;
}
#Override
public String toString() {
return "X{" +
"keys=" + keys +
'}';
}
}
}
But you will have to create one more super class if you want to use default A deserializer or look here how you can solve this: https://stackoverflow.com/a/18405958/1032167
If I understoon correctly, simply passing the values will work, without any config. I believe this is what you are looking for:
public class Test {
private static final String JSON = "{\"keys\":{\"primary_key\":\"abc\",\"secondary_key\":\"xyz\"}}";
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
X x = mapper.readValue(JSON, X.class);
System.out.println(mapper.writeValueAsString(x));
}
}
class A {
private String primary_key;
public String getPrimary_key() {
return primary_key;
}
public void setPrimary_key(String primary_key) {
this.primary_key = primary_key;
}
}
class B extends A {
private String secondary_key;
public String getSecondary_key() {
return secondary_key;
}
public void setSecondary_key(String secondary_key) {
this.secondary_key = secondary_key;
}
}
class X {
private B keys;
public B getKeys() {
return keys;
}
public void setKeys(B keys) {
this.keys = keys;
}
}
Output will be:
{"keys":{"primary_key":"abc","secondary_key":"xyz"}}
In case this is not what you expect, please provide another explanation and I will edit the answer as needed.

JSON data binding with custom logic using Jackson

I have defined JSON response which I want to deserialize into Java Objects. I managed to do it "manually" with the Tree Model but if possible I would like to use Data Binding instead. The problem is that I need some custom logic for some parts.
The JSON looks like this:
{
"resourcedescriptions": [
{
"path": "somePath",
"tag_pagetype": "default",
"tag_bookingcenter": [
"bc_ch",
"bc_de"
],
"resources": [
{
"path": "somePathDe.html",
"lang": "de",
"lastmodified": 1399020442914,
"mimetype": "text/html"
},
{
"path": "somePathEn.html",
"lang": "en",
"lastmodified": 1399907224208,
"mimetype": "text/html"
}
],
"lastmodified": 1399907224208
},
{
"path": "someOtherPath",
"tag_pagetype": "special",
"tag_bookingcenter": [
"bc_ch"
],
"resources": [
{
"path": "someOtherPathDe.html",
"lang": "de",
"lastmodified": 1399020442914,
"mimetype": "text/html"
},
{
"path": "someOtherPathEn.html",
"lang": "en",
"lastmodified": 1399907224208,
"mimetype": "text/html"
}
],
"lastmodified": 1399907224208
}
]
}
My Java Classes would be:
public class ResourceDescription {
private String path;
private LocalDateTime lastModified;
private String chartConfig;
private final List<Tag> tags = new ArrayList<Tag>();
private final List<Resource> resources = new ArrayList<Resource>();
}
public class Resource {
private String lang;
private String path;
private String mimeType;
private LocalDateTime lastModified;
}
public class Tag {
private String namespace;
private String name;
}
First question which I still don't fully understand even with reading many posts here. How do I deserialize this array of Resources from the JSON into my List of the ResourceDescription?
Second and most complex question. The JSON properties prefixed with "tag_" need to be transformed into the Tag class, whereas the the property name represents the namespace and the value (single or array) represent the name. So if the pattern is "namespace:name", the first ResourceDescription would have the following tags:
tag_pagetype:default
tag_bookingcenter:bc_ch
tag_bookingcenter:bc_de
Third the "lastmodified" should be transformed into DateTime from Joda-Time.
Is this even possible with data binding or should I stick to the Tree Model?
How do I deserialize this array of Resources from the JSON into my
List of the ResourceDescription?
You have to create additional root class which contains resourcedescriptions property. For example:
class Root {
private List<ResourceDescription> resourcedescriptions;
public List<ResourceDescription> getResourcedescriptions() {
return resourcedescriptions;
}
public void setResourcedescriptions(List<ResourceDescription> resourcedescriptions) {
this.resourcedescriptions = resourcedescriptions;
}
#Override
public String toString() {
return String.valueOf(resourcedescriptions);
}
}
The JSON properties prefixed with "tag_" need to be transformed into
the Tag class, whereas the the property name represents the namespace
and the value (single or array) represent the name.
You can handle this case using #JsonAnySetter annotation. You have to add new method to ResourceDescription class which could look like this:
#JsonAnySetter
public void setAnyValues(String propertyName, Object value) {
if (propertyName.startsWith("tag_")) {
if (value instanceof String) {
tags.add(new Tag(propertyName, value.toString()));
} else if (value instanceof List) {
List<?> values = (List<?>) value;
for (Object v : values) {
tags.add(new Tag(propertyName, v.toString()));
}
}
// throw exception?
} else {
// handle another unknown properties
}
}
Third the "lastmodified" should be transformed into DateTime from
Joda-Time.
You can handle JodaTime types by adding jackson-datatype-joda library. When you add it you can register JodaModule module.
mapper.registerModule(new JodaModule());
Additional problem that your JSON contain properties written using lowercase, but your POJO properties are written using camel-case. You can change JSON or POJO or use #JsonProperty("property-name-from-JSON") annotation or implement your own naming strategy. For example:
mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
#Override
public String translate(String propertyName) {
return propertyName.toLowerCase();
}
});
Full Java example how to you can deserialize your JSON:
import java.util.ArrayList;
import java.util.List;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.datatype.joda.JodaModule;
public class JacksonProgram {
public static void main(String[] args) throws Exception {
String json = "{ ... }";
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
#Override
public String translate(String propertyName) {
return propertyName.toLowerCase();
}
});
System.out.println(mapper.readValue(json, Root.class));
}
}
class Root {
private List<ResourceDescription> resourcedescriptions;
public List<ResourceDescription> getResourcedescriptions() {
return resourcedescriptions;
}
public void setResourcedescriptions(List<ResourceDescription> resourcedescriptions) {
this.resourcedescriptions = resourcedescriptions;
}
#Override
public String toString() {
return String.valueOf(resourcedescriptions);
}
}
class ResourceDescription {
private String path;
private LocalDateTime lastModified;
private String chartConfig;
private final List<Tag> tags = new ArrayList<Tag>();
private final List<Resource> resources = new ArrayList<Resource>();
#JsonAnySetter
public void setAnyValues(String propertyName, Object value) {
if (propertyName.startsWith("tag_")) {
if (value instanceof String) {
tags.add(new Tag(propertyName, value.toString()));
} else if (value instanceof List) {
List<?> values = (List<?>) value;
for (Object v : values) {
tags.add(new Tag(propertyName, v.toString()));
}
}
// throw exception?
} else {
// handle another unknown properties
}
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public LocalDateTime getLastModified() {
return lastModified;
}
public void setLastModified(LocalDateTime lastModified) {
this.lastModified = lastModified;
}
public String getChartConfig() {
return chartConfig;
}
public void setChartConfig(String chartConfig) {
this.chartConfig = chartConfig;
}
public List<Tag> getTags() {
return tags;
}
public List<Resource> getResources() {
return resources;
}
#Override
public String toString() {
return "ResourceDescription [path=" + path + ", lastModified=" + lastModified
+ ", chartConfig=" + chartConfig + ", tags=" + tags + ", resources=" + resources
+ "]";
}
}
class Resource {
private String lang;
private String path;
private String mimeType;
private LocalDateTime lastModified;
public String getLang() {
return lang;
}
public void setLang(String lang) {
this.lang = lang;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public LocalDateTime getLastModified() {
return lastModified;
}
public void setLastModified(LocalDateTime lastModified) {
this.lastModified = lastModified;
}
#Override
public String toString() {
return "Resource [lang=" + lang + ", path=" + path + ", mimeType=" + mimeType
+ ", lastModified=" + lastModified + "]";
}
}
class Tag {
private String namespace;
private String name;
public Tag() {
}
public Tag(String namespace, String name) {
this.namespace = namespace;
this.name = name;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public String toString() {
return "Tag [namespace=" + namespace + ", name=" + name + "]";
}
}
Above program prints:
[ResourceDescription [path=somePath, lastModified=2014-05-12T17:07:04.208, chartConfig=null, tags=[Tag [namespace=tag_pagetype, name=default], Tag [namespace=tag_bookingcenter, name=bc_ch], Tag [namespace=tag_bookingcenter, name=bc_de]], resources=[Resource [lang=de, path=somePathDe.html, mimeType=text/html, lastModified=2014-05-02T10:47:22.914], Resource [lang=en, path=somePathEn.html, mimeType=text/html, lastModified=2014-05-12T17:07:04.208]]], ResourceDescription [path=someOtherPath, lastModified=2014-05-12T17:07:04.208, chartConfig=null, tags=[Tag [namespace=tag_pagetype, name=special], Tag [namespace=tag_bookingcenter, name=bc_ch]], resources=[Resource [lang=de, path=someOtherPathDe.html, mimeType=text/html, lastModified=2014-05-02T10:47:22.914], Resource [lang=en, path=someOtherPathEn.html, mimeType=text/html, lastModified=2014-05-12T17:07:04.208]]]]
You will need to create a custom deserializer for ResourceDescription in order to accomplish what you need to do. The syntax for specifying a custom deserializer for ResourceDescription will look like this:
#JsonDeserialize(using=ResourceDescriptionDeserializer.class)
public class ResourceDescription { ... }
This deserializer will have to iterate through each of the keys for each resource description to see if it begins with "tag_", strip off the prefix and use the remaining for the namespace and populate the name/value for the Tag before adding it to the array of the ResourceDescription being created.
For all other attributes/types I think you can just defer to the default deserialization and set those attributes on their respective fields.
Then, to deserialize the list of ResourceDescriptions you can specify a TypeReference to avoid writing a custom deserializer for ResourceDescriptions. The code will look something like this:
Map<String, List<ResourceDescription>> resultMap =
objectMapper.readValue(JSON, new TypeReference<Map<String, List<ResourceDescription>>>() {});
List<ResourceDescription> descriptions = resultMap.get("resourcedescriptions");
Here's an article that doesn't quite pair with what you're doing but I think will help with the general idea:
Using Jackson to deserialize array nested within array in JSON object

Json API Parsing troubles with Java

I'm running into a few issues similar to what others have had in the past with Json parsing in Java. This is the first time I try something like this so any help/tips is extremely useful.
I'm trying to parse in data from this site: https://api.bitcoinaverage.com/exchanges/USD
I have tried numerous ways with both Json and Gson. And have tried looking for help here but to no avail.
Here are the classes that are set up (these were auto generated):
Info.java:
public class Info{
private String display_URL;
private String display_name;
private Rates[] rates;
private String source;
private Number volume_btc;
private Number volume_percent;
public String getDisplay_URL(){
return this.display_URL;
}
public void setDisplay_URL(String display_URL){
this.display_URL = display_URL;
}
public String getDisplay_name(){
return this.display_name;
}
public void setDisplay_name(String display_name){
this.display_name = display_name;
}
public Rates[] getRates(){
return this.rates;
}
public void setRates(Rates[] rates){
this.rates = rates;
}
public String getSource(){
return this.source;
}
public void setSource(String source){
this.source = source;
}
public Number getVolume_btc(){
return this.volume_btc;
}
public void setVolume_btc(Number volume_btc){
this.volume_btc = volume_btc;
}
public Number getVolume_percent(){
return this.volume_percent;
}
public void setVolume_percent(Number volume_percent){
this.volume_percent = volume_percent;
}
}
Rates.java:
public class Rates {
private Number ask;
private Number bid;
private Number last;
public Number getAsk(){
return this.ask;
}
public void setAsk(Number ask){
this.ask = ask;
}
public Number getBid(){
return this.bid;
}
public void setBid(Number bid){
this.bid = bid;
}
public Number getLast(){
return this.last;
}
public void setLast(Number last){
this.last = last;
}
}
MainClass.java:
public class MainClass {
public static void main(String[] args) throws Exception {
Gson gson = new Gson();
String json = readUrl("https://api.bitcoinaverage.com/exchanges/USD");
Info page = gson.fromJson(json, Info.class);
System.out.println(page.getDisplay_name());
}
private static String readUrl(String urlString) throws Exception {
BufferedReader reader = null;
try {
URL url = new URL(urlString);
reader = new BufferedReader(new InputStreamReader(url.openStream()));
StringBuffer buffer = new StringBuffer();
int read;
char[] chars = new char[1024];
while ((read = reader.read(chars)) != -1)
buffer.append(chars, 0, read);
return buffer.toString();
} finally {
if (reader != null)
reader.close();
}
}
}
When I try to call a getter, a null is returned.
How do I go about parsing the data properly, and then being able to call an attribute from which ever object I want? For example, if I want an attribute from "anx_hk" or "bitfinex".
This is the first time me posting something here so I hope I'm following the proper guidelines.
I also plan on passing this over to Android once I get the fell for parsing Json better. Thanks for the help! It'll greatly be appreciated.
I'll be honest with you, that's a pretty lame API response. Here it is
{
"anx_hk": {
"display_URL": "https://anxbtc.com/",
"display_name": "ANXBTC",
"rates": {
"ask": 454.26,
"bid": 444.46,
"last": 443.78
},
"source": "bitcoincharts",
"volume_btc": 11.73,
"volume_percent": 0.02
},
...,
"timestamp": "Fri, 04 Apr 2014 04:30:26 -0000",
...
}
There's no JSON array here, so you can get rid of all your array types. This response is a JSON object, which contains a bunch of JSON objects (which share a format) and a JSON name value pair where the name is timestamp.
The common JSON objects have two fields of type double (that's what type your field should be, not Number)
"volume_btc": 11.73,
"volume_percent": 0.02
, three fields of type String
"display_URL": "https://anxbtc.com/",
"display_name": "ANXBTC",
"source": "bitcoincharts",
and one that is a JSON object that contains three more doubles
"rates": {
"ask": 454.26,
"bid": 444.46,
"last": 443.78
}
The actual issue here is that, I'm assuming, the JSON objects in the root JSON object have names that may change or new ones may be added. This is not a good fit for a POJO. Instead you'd want to use a Map<String, Info>, but Gson can't map to that by default. It is not well suited for such deserialization. You'd have to provide your own TypeAdapter.
Instead, I'm going to suggest you use Jackson.
If we put that all together, we get something like
class ApiResponse {
private Map<String, Info> page = new HashMap<>();
private Date timestamp;
public Map<String, Info> getPage() {
return page;
}
#JsonAnySetter
public void setPage(String name, Info value) {
page.put(name, value);
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
}
class Info {
private String display_URL;
private String display_name;
private Rates rates;
private String source;
private Double volume_btc;
private Double volume_percent;
public String getDisplay_URL() {
return this.display_URL;
}
public void setDisplay_URL(String display_URL) {
this.display_URL = display_URL;
}
public String getDisplay_name() {
return this.display_name;
}
public void setDisplay_name(String display_name) {
this.display_name = display_name;
}
public Rates getRates() {
return this.rates;
}
public void setRates(Rates rates) {
this.rates = rates;
}
public String getSource() {
return this.source;
}
public void setSource(String source) {
this.source = source;
}
public Double getVolume_btc() {
return this.volume_btc;
}
public void setVolume_btc(Double volume_btc) {
this.volume_btc = volume_btc;
}
public Double getVolume_percent() {
return this.volume_percent;
}
public void setVolume_percent(Double volume_percent) {
this.volume_percent = volume_percent;
}
}
class Rates {
private Double ask;
private Double bid;
private Double last;
public Number getAsk() {
return this.ask;
}
public void setAsk(Double ask) {
this.ask = ask;
}
public Double getBid() {
return this.bid;
}
public void setBid(Double bid) {
this.bid = bid;
}
public Double getLast() {
return this.last;
}
public void setLast(Double last) {
this.last = last;
}
}
With deserialization code such as
String json = readUrl("https://api.bitcoinaverage.com/exchanges/USD");
ObjectMapper mapper = new ObjectMapper();
ApiResponse response = mapper.readValue(json, ApiResponse.class);
System.out.println(response);
With appropriate toString() methods (mine were auto-generated with Eclipse), you would get something like
ApiResponse [pages={bitkonan=Info [display_URL=https://bitkonan.com/, display_name=BitKonan, rates=Rates [ask=475.0, bid=438.01, last=437.0], source=api, volume_btc=7.24, volume_percent=0.01], vaultofsatoshi=Info [display_URL=https://vaultofsatoshi.com, display_name=Vault of Satoshi, rates=Rates [ask=460.0, bid=460.0, last=460.0], source=api, volume_btc=11.46, volume_percent=0.02], bitstamp=Info [display_URL=https://bitstamp.net/, display_name=Bitstamp, rates=Rates [ask=439.16, bid=436.34, last=436.34], source=api, volume_btc=22186.29, volume_percent=35.19], ...}, timestamp=Fri Apr 04 01:02:43 EDT 2014]
as output.
The api response contains many objects, but seems that you are trying to read them as a single Info object.
You may try to read the response as a Map<String, Info>, and iterate the entries.
Map<String, Info> hashMap = gson.fromJson(body, HashMap.class);
for (Map.Entry entry : hashMap.entrySet()) {
// your code
}

Categories