I have a very complex Json object that I get as a String:
{ "a": ..., "b":..., /* lots of other properties */ "z":... }
that I read partially with Jackson and map into a Java class:
class PartialObjectForB { #JsonProperty("b") private ObjectB b; }
I use the readValue() method from the ObjectMapper class and get what I want... So far, so good.
Now, I want to update some values in PartialObjectForB and update the initial string I had.
I figured how to update a Java object with jackson (by using readerForUpdating) but can't find how to do the opposite: update a Json object/string with a Java object.
I know how to solve quickly that problem by using JSONObject. For example, if I just want to update 1 value:
JSONObject j = new JSONObject(/* the full json string */);
j.getJSONObject("b").getJSONObject("bb")/* etc. */.put("bbbb", 4);
j.toString(); // will give me the full original text with only "b" updated.
But can't find how to do it with jackson.
Any idea?
Notes:
My input/output are strings, can't change that.
I don't know what data is in the json object. I just know that I may have the property "b" and that if I don't I can create it.
I may want to deserialize and update more than 1 property at the root level (e.g: "b", "h" and "w").
This problem is not recursive. Meaning: I have a full representation of the values I unserialize (no unknown properties).
The json object, as a string, is made of a few thousand bytes, but the piece(s) I want to update is usually a lot smaller (e.g: around 100 bytes).
Full executable source with benchmark included is:
import java.io.IOException;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Random;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.Module;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.introspect.BasicBeanDescription;
import org.codehaus.jackson.map.ser.BeanPropertyWriter;
import org.codehaus.jackson.map.ser.std.BeanSerializerBase;
import org.codehaus.jackson.node.ObjectNode;
import org.json.JSONException;
import org.json.JSONObject;
public class JacksonModule {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final int COUNT = 0;
private static final int REPEAT_HEADER = 40;
static {
MAPPER.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MAPPER.configure(SerializationConfig.Feature.WRITE_NULL_PROPERTIES, false);
MAPPER.registerModule(new MyModule());
}
private DataProcessor sdp;
private long[] sum = new long[5];
public static void main(String[] args) throws IOException, JSONException {
new JacksonModule().start();
}
public JacksonModule() throws IOException, JSONException {
this.sdp = new DataProcessor();
}
public void start() throws IOException, JSONException {
run(-1, false); // load classes: slow
if (COUNT > 0) {
for (int i = 0; i < COUNT; ++i) {
if (i % REPEAT_HEADER == 0) {
System.out.println("---------------------------------------------------------------------------------------");
print("", "RO JSONObject", "RO Jackson", "R/- Jackson", "R/W JSONObject", "R/W Jackson");
System.out.println("---------------------------------------------------------------------------------------");
}
run(i, true);
}
System.out.println("-- AVERAGE ----------------------------------------------------------------------------");
print(1, sum[0] / COUNT, sum[1] / COUNT, sum[2] / COUNT, sum[3] / COUNT, sum[4] / COUNT);
System.out.println("---------------------------------------------------------------------------------------");
print("", "RO JSONObject", "RO Jackson", "R/- Jackson", "R/W JSONObject", "R/W Jackson");
System.out.println("---------------------------------------------------------------------------------------");
}
}
public void run(int i, boolean print) throws JSONException, IOException {
long t1 = sdp.doReadWithJSONObject();
long t2 = sdp.doReadWithJackson();
long t3 = sdp.doReadForUpdatingWithJacksonButDontWrite();
long t4 = sdp.doSomeWritingWithJSONObject();
long t5 = sdp.doSomeWritingWithJackson();
if (print) {
print(i, t1, t2, t3, t4, t5);
sum[0] += t1;
sum[1] += t2;
sum[2] += t3;
sum[3] += t4;
sum[4] += t5;
}
}
private void print(int index, long t1, long t2, long t3, long t4, long t5) {
print(Integer.toString(index), String.format("%,d", t1), String.format("%,d", t2), String.format("%,d", t3), String.format("%,d", t4), String.format("%,d", t5));
}
private void print(String i0, String a, String b, String c, String d, String e) {
System.out.println("|"
+ StringUtils.leftPad(i0, 5) + "|"
+ StringUtils.leftPad(a, 15) + "|"
+ StringUtils.leftPad(b, 15) + "|"
+ StringUtils.leftPad(c, 15) + "|"
+ StringUtils.leftPad(d, 15) + "|"
+ StringUtils.leftPad(e, 15) + "|");
}
private static class DataProcessor {
private DataStore store;
private long t0, t1;
private DataProcessor() throws IOException, JSONException {
this.store = new DataStore(customer, browser);
}
public long doReadWithJSONObject() throws JSONException {
t0 = System.nanoTime();
JSONObject json = new JSONObject(store.readData(null)); // can throw JSONException
JSONObject customer = json.getJSONObject("customer"); // can throw JSONException
JSONObject browserInfo = json.getJSONObject("browser"); // can throw JSONException
// need to do manually the mapping and figure out what is exactly in this object. Hell no!
t1 = System.nanoTime();
return t1 - t0;
}
public long doReadWithJackson() throws IOException {
t0 = System.nanoTime();
KnownPart obj = store.readData(null, KnownPart.class);
t1 = System.nanoTime();
return t1 - t0;
}
public long doReadForUpdatingWithJacksonButDontWrite() throws IOException {
t0 = System.nanoTime();
KnownPart obj = store.readDataForUpdating(null, KnownPart.class);
t1 = System.nanoTime();
return t1 - t0;
}
public long doSomeWritingWithJSONObject() throws JSONException {
t0 = System.nanoTime();
JSONObject json = new JSONObject(store.readData(null)); // can throw JSONException
JSONObject customer = json.getJSONObject("customer"); // can throw JSONException
JSONObject browserInfo = json.getJSONObject("browser"); // can throw JSONException
customer.put("name", "Jackson Doe");
browserInfo.put("version", "10");
store.saveData(json);
t1 = System.nanoTime();
return t1 - t0;
}
public long doSomeWritingWithJackson() throws IOException {
t0 = System.nanoTime();
KnownPart obj = store.readDataForUpdating(null, KnownPart.class);
obj.customer.name = "Jackson Doe";
obj.browser.version = "10";
store.saveData(obj);
t1 = System.nanoTime();
return t1 - t0;
}
}
private static class DataStore {
private final String data;
private DataStore(Customer customer, BrowserInfo browser) throws IOException, JSONException {
StringWriter sw = new StringWriter(1000);
try (JsonGenerator jgen = MAPPER.getJsonFactory().createJsonGenerator(sw)) {
jgen.writeStartObject();
writeBunchOfProperties(jgen);
jgen.writeFieldName("customer");
jgen.writeRawValue(MAPPER.writeValueAsString(customer));
writeBunchOfProperties(jgen);
jgen.writeFieldName("browser");
jgen.writeRawValue(MAPPER.writeValueAsString(browser));
writeBunchOfProperties(jgen);
jgen.writeEndObject();
}
this.data = sw.toString();
}
private void writeBunchOfProperties(JsonGenerator jgen) throws IOException {
int c = new Random().nextInt(3) + 1;
for (int i = 0; i < c; ++i) {
jgen.writeFieldName(RandomStringUtils.random(10));
jgen.writeRawValue(JSON_LONG);
}
}
public String readData(String query) {
return data;
}
private void saveData(String json) {
// TODO
}
public void saveData(JSONObject json) {
saveData(json.toString());
}
public void saveData(Object obj) throws IOException {
saveData(MAPPER.writeValueAsString(obj));
}
public <T> T readData(String query, Class<T> clazz) throws IOException {
return MAPPER.readValue(readData(query), clazz);
}
public <T extends UnknownPart> T readDataForUpdating(String query, Class<T> clazz) throws IOException {
ObjectNode root = (ObjectNode) MAPPER.readTree(readData(query));
T obj = MAPPER.readValue(root, clazz);
obj.tree = root;
return obj;
}
}
private static abstract class UnknownPart {
ObjectNode tree;
}
private static class KnownPart extends UnknownPart {
#JsonProperty
private Customer customer;
#JsonProperty
private BrowserInfo browser;
}
private static class Customer {
#JsonProperty
private int id;
#JsonProperty
private String name;
#JsonProperty
private Address[] addresses; // just to make it more complex for this example
public Customer(int id, String name, Address[] addresses) {
this.id = id;
this.name = name;
this.addresses = addresses;
}
public Customer() {
}
}
private static class Address {
#JsonProperty
private String street;
#JsonProperty
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public Address() {
}
}
private static class BrowserInfo {
#JsonProperty
private String agent;
#JsonProperty
private String version;
public BrowserInfo(String agent, String version) {
this.agent = agent;
this.version = version;
}
public BrowserInfo() {
}
}
private static class MyModule extends Module {
#Override
public String getModuleName() {
return "MyModule";
}
#Override
public Version version() {
return new Version(0, 0, 1, "SNAPSHOT");
}
#Override
public void setupModule(Module.SetupContext context) {
context.addBeanSerializerModifier(new org.codehaus.jackson.map.ser.BeanSerializerModifier() {
private UnknownPartSerializer cs;
#Override
public JsonSerializer modifySerializer(SerializationConfig config, BasicBeanDescription beanDesc, JsonSerializer<?> serializer) {
return UnknownPart.class.isAssignableFrom(beanDesc.getBeanClass())
? new UnknownPartSerializer((BeanSerializerBase) serializer)
: serializer;
}
});
}
}
private static class UnknownPartSerializer extends BeanSerializerBase {
public UnknownPartSerializer(BeanSerializerBase src) {
super(src);
}
#Override
public void serialize(Object bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
UnknownPart up = (UnknownPart) bean;
jgen.writeStartObject();
serializeFields(up, jgen, provider);
jgen.writeEndObject();
}
protected void serializeFields(UnknownPart bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
final BeanPropertyWriter[] props;
if (_filteredProps != null && provider.getSerializationView() != null) {
props = _filteredProps;
} else {
props = _props;
}
int i = 0;
try {
for (final int len = props.length; i < len; ++i) {
BeanPropertyWriter prop = props[i];
if (prop != null) { // can have nulls in filtered list
prop.serializeAsField(bean, jgen, provider);
bean.tree.remove(prop.getName());
}
}
if (_anyGetterWriter != null) {
_anyGetterWriter.getAndSerialize(bean, jgen, provider);
}
Iterator<Entry<String, JsonNode>> it = bean.tree.getFields();
while (it.hasNext()) {
Entry<String, JsonNode> e = it.next();
jgen.writeFieldName(e.getKey());
jgen.writeObject(e.getValue());
}
} catch (Exception e) {
String name = (i == props.length) ? "[anySetter]"
: props[i].getName();
wrapAndThrow(provider, e, bean, name);
} catch (StackOverflowError e) {
/* 04-Sep-2009, tatu: Dealing with this is tricky, since we do not
* have many stack frames to spare... just one or two; can't
* make many calls.
*/
JsonMappingException mapE = new JsonMappingException("Infinite recursion (StackOverflowError)", e);
String name = (i == props.length) ? "[anySetter]"
: props[i].getName();
mapE.prependPath(new JsonMappingException.Reference(bean, name));
throw mapE;
}
}
}
private static Customer customer = new Customer(1, "John Doe", new Address[]{
new Address("broadway av", "new york"),
new Address("peachtree st", "atlanta")
});
private static BrowserInfo browser = new BrowserInfo("IE", "6.0");
// some json found on the internet
private static final String JSON_LONG = "{\"web-app\": {"
+ "\"servlet\": ["
+ "{"
+ "\"servlet-name\": \"cofaxCDS\","
+ "\"servlet-class\": \"org.cofax.cds.CDSServlet\","
+ "\"init-param\": {"
+ "\"configGlossary:installationAt\": \"Philadelphia, PA\","
+ "\"configGlossary:adminEmail\": \"ksm#pobox.com\","
+ "\"configGlossary:poweredBy\": \"Cofax\","
+ "\"configGlossary:poweredByIcon\": \"/images/cofax.gif\","
+ "\"configGlossary:staticPath\": \"/content/static\","
+ "\"templateProcessorClass\": \"org.cofax.WysiwygTemplate\","
+ "\"templateLoaderClass\": \"org.cofax.FilesTemplateLoader\","
+ "\"templatePath\": \"templates\","
+ "\"templateOverridePath\": \"\","
+ "\"defaultListTemplate\": \"listTemplate.htm\","
+ "\"defaultFileTemplate\": \"articleTemplate.htm\","
+ "\"useJSP\": false,"
+ "\"jspListTemplate\": \"listTemplate.jsp\","
+ "\"jspFileTemplate\": \"articleTemplate.jsp\","
+ "\"cachePackageTagsTrack\": 200,"
+ "\"cachePackageTagsStore\": 200,"
+ "\"cachePackageTagsRefresh\": 60,"
+ "\"cacheTemplatesTrack\": 100,"
+ "\"cacheTemplatesStore\": 50,"
+ "\"cacheTemplatesRefresh\": 15,"
+ "\"cachePagesTrack\": 200,"
+ "\"cachePagesStore\": 100,"
+ "\"cachePagesRefresh\": 10,"
+ "\"cachePagesDirtyRead\": 10,"
+ "\"searchEngineListTemplate\": \"forSearchEnginesList.htm\","
+ "\"searchEngineFileTemplate\": \"forSearchEngines.htm\","
+ "\"searchEngineRobotsDb\": \"WEB-INF/robots.db\","
+ "\"useDataStore\": true,"
+ "\"dataStoreClass\": \"org.cofax.SqlDataStore\","
+ "\"redirectionClass\": \"org.cofax.SqlRedirection\","
+ "\"dataStoreName\": \"cofax\","
+ "\"dataStoreDriver\": \"com.microsoft.jdbc.sqlserver.SQLServerDriver\","
+ "\"dataStoreUrl\": \"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\","
+ "\"dataStoreUser\": \"sa\","
+ "\"dataStorePassword\": \"dataStoreTestQuery\","
+ "\"dataStoreTestQuery\": \"SET NOCOUNT ON;select test='test';\","
+ "\"dataStoreLogFile\": \"/usr/local/tomcat/logs/datastore.log\","
+ "\"dataStoreInitConns\": 10,"
+ "\"dataStoreMaxConns\": 100,"
+ "\"dataStoreConnUsageLimit\": 100,"
+ "\"dataStoreLogLevel\": \"debug\","
+ "\"maxUrlLength\": 500}},"
+ "{"
+ "\"servlet-name\": \"cofaxEmail\","
+ "\"servlet-class\": \"org.cofax.cds.EmailServlet\","
+ "\"init-param\": {"
+ "\"mailHost\": \"mail1\","
+ "\"mailHostOverride\": \"mail2\"}},"
+ "{"
+ "\"servlet-name\": \"cofaxAdmin\","
+ "\"servlet-class\": \"org.cofax.cds.AdminServlet\"},"
+ ""
+ "{"
+ "\"servlet-name\": \"fileServlet\","
+ "\"servlet-class\": \"org.cofax.cds.FileServlet\"},"
+ "{"
+ "\"servlet-name\": \"cofaxTools\","
+ "\"servlet-class\": \"org.cofax.cms.CofaxToolsServlet\","
+ "\"init-param\": {"
+ "\"templatePath\": \"toolstemplates/\","
+ "\"log\": 1,"
+ "\"logLocation\": \"/usr/local/tomcat/logs/CofaxTools.log\","
+ "\"logMaxSize\": \"\","
+ "\"dataLog\": 1,"
+ "\"dataLogLocation\": \"/usr/local/tomcat/logs/dataLog.log\","
+ "\"dataLogMaxSize\": \"\","
+ "\"removePageCache\": \"/content/admin/remove?cache=pages&id=\","
+ "\"removeTemplateCache\": \"/content/admin/remove?cache=templates&id=\","
+ "\"fileTransferFolder\": \"/usr/local/tomcat/webapps/content/fileTransferFolder\","
+ "\"lookInContext\": 1,"
+ "\"adminGroupID\": 4,"
+ "\"betaServer\": true}}],"
+ "\"servlet-mapping\": {"
+ "\"cofaxCDS\": \"/\","
+ "\"cofaxEmail\": \"/cofaxutil/aemail/*\","
+ "\"cofaxAdmin\": \"/admin/*\","
+ "\"fileServlet\": \"/static/*\","
+ "\"cofaxTools\": \"/tools/*\"},"
+ ""
+ "\"taglib\": {"
+ "\"taglib-uri\": \"cofax.tld\","
+ "\"taglib-location\": \"/WEB-INF/tlds/cofax.tld\"}}}";
}
Victory! :) I have now an implementation for it and it is not quite simple.
Benchmark Jackson vs JSONObject included.
Problem and solution described piece by piece:
First, I have a big json string in a data store that I want to partially deserialize and update. The deserialization has to be partial, but serialization full, so I don't lose the data I didn't deserialize.
Here are the object I am using for the example:
private static class KnownPart {
#JsonProperty
private Customer customer;
#JsonProperty
private BrowserInfo browser;
}
private static class Customer {
#JsonProperty
private int id;
#JsonProperty
private String name;
#JsonProperty
private Address[] addresses; // just to make it more complex for this example
public Customer(int id, String name, Address[] addresses) {
this.id = id;
this.name = name;
this.addresses = addresses;
}
public Customer() {
}
}
private static class Address {
#JsonProperty
private String street;
#JsonProperty
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public Address() {
}
}
private static class BrowserInfo {
#JsonProperty
private String agent;
#JsonProperty
private String version;
public BrowserInfo(String agent, String version) {
this.agent = agent;
this.version = version;
}
public BrowserInfo() {
}
}
KnownPart object contains a lot more than 2 properties, but I have no idea which ones exactly and I can not guarantee to keep track of which properties are added to the json string...
I have a data store which has its data encoded in Json. The current implementation uses JSONObject to read/write and supports jackson to read json and map it to "real" Java object (POJOs and more complex objects).
While writing full Json strings with jackson is not a problem, updating an existing string with a Java object that only represents a small part of it is much harder.
Data store problem:
private static class DataStore {
private final String data;
private DataStore(Customer customer, BrowserInfo browser) throws IOException, JSONException {
StringWriter sw = new StringWriter(1000);
try (JsonGenerator jgen = MAPPER.getJsonFactory().createJsonGenerator(sw)) {
jgen.writeStartObject();
writeBunchOfProperties(jgen);
jgen.writeFieldName("customer");
jgen.writeRawValue(MAPPER.writeValueAsString(customer));
writeBunchOfProperties(jgen);
jgen.writeFieldName("browser");
jgen.writeRawValue(MAPPER.writeValueAsString(browser));
writeBunchOfProperties(jgen);
jgen.writeEndObject();
}
this.data = sw.toString();
}
private void writeBunchOfProperties(JsonGenerator jgen) throws IOException {
int c = new Random().nextInt(3) + 1;
for (int i = 0; i < c; ++i) {
jgen.writeFieldName(RandomStringUtils.random(10));
jgen.writeRawValue(JSON_LONG);
}
}
public String readData(String query) {
return data;
}
private void saveData(String json) {
// Not implemented
}
public void saveData(JSONObject json) {
saveData(json.toString());
}
public void saveData(Object obj) throws IOException {
// problem: ?
}
public <T> T readData(String query, Class<T> clazz) throws IOException {
return MAPPER.readValue(readData(query), clazz);
}
public <T> T readDataForUpdating(String query, Class<T> clazz) throws IOException {
// problem: ?
}
At the higher level, I want to be able to do something like:
public long doSomeWritingWithJackson() throws IOException {
t0 = System.nanoTime();
KnownPart obj = store.readDataForUpdating(null, KnownPart.class);
obj.customer.name = "Jackson Doe";
obj.browser.version = "10";
store.saveData(obj);
t1 = System.nanoTime();
return t1 - t0;
}
without losing the data I didn't read when I save.
Obviously, I don't want to read a 2nd time the data because I'm reading this from a remote host and I don't want to cache it in some static/instance map because I need this to be still very effective in a highly concurrent environment.
So the solution, in a few words is:
- to read the tree of the json string first and use it to deserialize the Json object into the Java object.
- store the deserialize object (in KnowPart class) and store the tree in some parent abstract class
- write a Jackson module to customize how bean are serialized. The code is pretty much the same than the original one with the difference that when an attribute from the KnownPart is written, it's key is removed the tree which is in the UnknownPart object, then it is easy to write the unknown part...
The main object becomes:
private static abstract class UnknownPart {
ObjectNode tree;
}
private static class KnownPart extends UnknownPart {
#JsonProperty
private Customer customer;
#JsonProperty
private BrowserInfo browser;
}
The module only deals with UnknownPart objects:
private static class MyModule extends Module {
#Override
public String getModuleName() {
return "MyModule";
}
#Override
public Version version() {
return new Version(0, 0, 1, "SNAPSHOT");
}
#Override
public void setupModule(Module.SetupContext context) {
context.addBeanSerializerModifier(new org.codehaus.jackson.map.ser.BeanSerializerModifier() {
private UnknownPartSerializer cs;
#Override
public JsonSerializer modifySerializer(SerializationConfig config, BasicBeanDescription beanDesc, JsonSerializer<?> serializer) {
return UnknownPart.class.isAssignableFrom(beanDesc.getBeanClass())
? new UnknownPartSerializer((BeanSerializerBase) serializer)
: serializer;
}
});
}
}
And the serializer is:
private static class UnknownPartSerializer extends BeanSerializerBase {
public UnknownPartSerializer(BeanSerializerBase src) {
super(src);
}
#Override
public void serialize(Object bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
UnknownPart up = (UnknownPart) bean;
jgen.writeStartObject();
serializeFields(up, jgen, provider);
jgen.writeEndObject();
}
protected void serializeFields(UnknownPart bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
final BeanPropertyWriter[] props;
if (_filteredProps != null && provider.getSerializationView() != null) {
props = _filteredProps;
} else {
props = _props;
}
int i = 0;
try {
for (final int len = props.length; i < len; ++i) {
BeanPropertyWriter prop = props[i];
if (prop != null) { // can have nulls in filtered list
prop.serializeAsField(bean, jgen, provider);
bean.tree.remove(prop.getName()); // new
}
}
if (_anyGetterWriter != null) {
_anyGetterWriter.getAndSerialize(bean, jgen, provider);
}
// new:
Iterator<Entry<String, JsonNode>> it = bean.tree.getFields();
while (it.hasNext()) {
Entry<String, JsonNode> e = it.next();
jgen.writeFieldName(e.getKey());
jgen.writeObject(e.getValue());
}
} catch (Exception e) {
String name = (i == props.length) ? "[anySetter]"
: props[i].getName();
wrapAndThrow(provider, e, bean, name);
} catch (StackOverflowError e) {
/* 04-Sep-2009, tatu: Dealing with this is tricky, since we do not
* have many stack frames to spare... just one or two; can't
* make many calls.
*/
JsonMappingException mapE = new JsonMappingException("Infinite recursion (StackOverflowError)", e);
String name = (i == props.length) ? "[anySetter]"
: props[i].getName();
mapE.prependPath(new JsonMappingException.Reference(bean, name));
throw mapE;
}
}
}
In the same time, I wrote a benchmark to confirm (or not) that this solution is faster than JSONObject for big Json strings...
The test compares:
- read with JSONObject (without mapping)
- read with Jackson
- read with tree with Jackson
- read/write with JSONObject
- read/write with Jackson
And it is indeed faster :)
After 1000 iterations, excluding potential class load or some initialization the JVM does, i get, in nano seconds:
-- AVERAGE ----------------------------------------------------------------------------
| 1| 860,560| 157,772| 234,654| 1,595,018| 488,427|
---------------------------------------------------------------------------------------
| | RO JSONObject| RO Jackson| R/- Jackson| R/W JSONObject| R/W Jackson|
---------------------------------------------------------------------------------------
The simplest solution which I can imagine - is deserializing your JSON into Map class (for example LinkedHashMap). Please, see my below example:
import java.util.LinkedHashMap;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonProgram {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
String json = "{\"a\":\"java.lang.Integer\",\"b\":\"time json\",\"c\":\"action json\",\"d\":[1,2,3]}";
System.out.println(json);
LinkedHashMap<String, Object> map = mapper.readValue(json, LinkedHashMap.class);
map.put("b", "Override property or create new");
System.out.println(mapper.writeValueAsString(map));
}
}
Above program prints:
{"a":"java.lang.Integer","b":"time json","c":"action json","d":[1,2,3]}
{"a":"java.lang.Integer","b":"Override property or create new","c":"action json","d":[1,2,3]}
If you want to change inner property on the path you can implement it in this way:
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonProgram {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String json = "{\"b\":{\"bb\":{\"bbb\":20}}}";
System.out.println(json);
LinkedHashMap<String, Object> map = mapper.readValue(json, LinkedHashMap.class);
JsonUpdater updater = new JsonUpdater(map);
updater.update(Arrays.asList("b", "bb", "bbb"), 4);
System.out.println(mapper.writeValueAsString(map));
}
}
class JsonUpdater {
private LinkedHashMap<String, Object> jsonMap;
public JsonUpdater(LinkedHashMap<String, Object> jsonMap) {
this.jsonMap = jsonMap;
}
public boolean update(Collection<String> propertiesOnThePath, Object newValue) {
LinkedList<String> path = new LinkedList<String>(propertiesOnThePath);
String lastProperty = path.removeLast();
LinkedHashMap<String, Object> objectMap = jsonMap;
while (!path.isEmpty()) {
String property = path.poll();
if (!objectMap.containsKey(property)) {
return false;
}
objectMap = (LinkedHashMap<String, Object>) objectMap.get(property);
}
if (!objectMap.containsKey(lastProperty)) {
return false;
}
objectMap.put(lastProperty, newValue);
return false;
}
}
Above program prints:
{"b":{"bb":{"bbb":20}}}
{"b":{"bb":{"bbb":4}}}
As we can see: value was changed. But this solution has huge disadvantage - we have to deserialize all JSON. Few thousands bytes String is not a problem for Java, but if you really want to optimaze your program you can play with ObjectNode class and ObjectMapper#readTree method. Please see below source code:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class JacksonProgram {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
String json = "{\"a\":\"java.lang.Integer\",\"b\":\"time json\",\"c\":\"action json\",\"d\":[1,2,3]}";
System.out.println(json);
ObjectNode jsonTree = (ObjectNode) mapper.readTree(json);
jsonTree.put("b", "Override property or create new");
System.out.println(jsonTree.toString());
}
}
Above program prints:
{"a":"java.lang.Integer","b":"time json","c":"action json","d":[1,2,3]}
{"a":"java.lang.Integer","b":"Override property or create new","c":"action json","d":[1,2,3]}
I didn't do any comparison tests, but you can test which solution works faster for you.
Related
Given the following POJOs ..
public class City {
private String title;
private List<Person> people;
}
...
public class Person {
private String name;
private int age;
}
I would like to let Jackson serialize instances of the classes to the following example JSON:
{
"title" : "New York",
"personName_1" : "Jane Doe",
"personAge_1" : 42,
"personName_2" : "John Doe",
"personAge_2" : 23
}
The JSON format is defined by an external API which I cannot change.
I already found that I can annotate the list field with a custom serializer such as:
#JsonSerialize(using = PeopleSerializer.class)
private List<Person> people;
... and here is a basic implementation I tried:
public class PeopleSerializer extends JsonSerializer<List<Person>> {
private static final int START_INDEX = 1;
#Override
public void serialize(List<Person> people,
JsonGenerator generator,
SerializerProvider provider) throws IOException {
for (int i = 0; i < people.size(); ++i) {
Person person = people.get(i);
int index = i + START_INDEX;
serialize(person, index, generator);
}
}
private void serialize(Person person, int index, JsonGenerator generator) throws IOException {
generator.writeStringField(getIndexedFieldName("personName", index),
person.getName());
generator.writeNumberField(getIndexedFieldName("personAge", index),
person.getAge());
}
private String getIndexedFieldName(String fieldName, int index) {
return fieldName + "_" + index;
}
}
However, this fails with an:
JsonGenerationException: Can not write a field name, expecting a value
I also looked into using Jackson's Converter interface but that's not suitable for unwrapping the nested list objects.
See: https://stackoverflow.com/a/41651324/356895
I am also aware of #JsonUnwrapped but it is not designed to be used with lists.
Related posts
Serialize List content in a flat structure in jackson json (Java)
Jackson: How to add custom property to the JSON without modifying the POJO
How to serialize only the ID of a child with Jackson
Related posts (deserialization)
Jackson list deserialization. nested Lists
Related library
Jackson JSON Interceptor Module
You can use the BeanSerializerModifier to directly modify how a property name and value are written. Using this you could detect if a custom annotation is present, in this case I made one called #FlattenCollection. When the annotation is present the array or collection is not written using the normal method but instead written by a custom property writer (FlattenCollectionPropertyWriter).
This annotation will likely break on 2d arrays or other edge cases, I havent tested those but you could probably code for them without to much trouble, at least throw a meaningful error.
Heres the full working code. Notable points are
FlattenCollectionSerializerModifier.changeProperties
FlattenCollectionPropertyWriter.serializeAsField
The couple TODOs i put in there for you.
Output:
{
"titleCity" : "New York",
"personName_1" : "Foo",
"personAge_1" : 123,
"personName_2" : "Baz",
"personAge_2" : 22
}
Code:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.*;
import com.fasterxml.jackson.databind.util.NameTransformer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.*;
public class SO45698499 {
public static void main(String [] args) throws Exception {
ObjectWriter writer = createMapper().writerWithDefaultPrettyPrinter();
String val = writer.writeValueAsString(new City("New York",
Arrays.asList(new Person("Foo", 123), new Person("Baz", 22))));
System.out.println(val);
}
/**
* Constructs our mapper with the serializer modifier in mind
* #return
*/
public static ObjectMapper createMapper() {
FlattenCollectionSerializerModifier modifier = new FlattenCollectionSerializerModifier();
SerializerFactory sf = BeanSerializerFactory.instance.withSerializerModifier(modifier);
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializerFactory(sf);
return mapper;
}
#Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface FlattenCollection {
}
/**
* Looks for the FlattenCollection annotation and modifies the bean writer
*/
public static class FlattenCollectionSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); i++) {
BeanPropertyWriter writer = beanProperties.get(i);
FlattenCollection annotation = writer.getAnnotation(FlattenCollection.class);
if (annotation != null) {
beanProperties.set(i, new FlattenCollectionPropertyWriter(writer));
}
}
return beanProperties;
}
}
/**
* Instead of writing a collection as an array, flatten the objects down into values.
*/
public static class FlattenCollectionPropertyWriter extends BeanPropertyWriter {
private final BeanPropertyWriter writer;
public FlattenCollectionPropertyWriter(BeanPropertyWriter writer) {
super(writer);
this.writer = writer;
}
#Override
public void serializeAsField(Object bean,
JsonGenerator gen,
SerializerProvider prov) throws Exception {
Object arrayValue = writer.get(bean);
// lets try and look for array and collection values
final Iterator iterator;
if(arrayValue != null && arrayValue.getClass().isArray()) {
// deal with array value
iterator = Arrays.stream((Object[])arrayValue).iterator();
} else if(arrayValue != null && Collection.class.isAssignableFrom(arrayValue.getClass())) {
iterator = ((Collection)arrayValue).iterator();
} else {
iterator = null;
}
if(iterator == null) {
// TODO: write null? skip? dunno, you gonna figure this one out
} else {
int index=0;
while(iterator.hasNext()) {
index++;
Object value = iterator.next();
if(value == null) {
// TODO: skip null values and still increment or maybe dont increment? You decide
} else {
// TODO: OP - update your prefix/suffix here, its kinda weird way of making a prefix
final String prefix = value.getClass().getSimpleName().toLowerCase();
final String suffix = "_"+index;
prov.findValueSerializer(value.getClass())
.unwrappingSerializer(new FlattenNameTransformer(prefix, suffix))
.serialize(value, gen, prov);
}
}
}
}
}
public static class FlattenNameTransformer extends NameTransformer {
private final String prefix;
private final String suffix;
public FlattenNameTransformer(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
#Override
public String transform(String name) {
// captial case the first letter, to prepend the suffix
String transformedName = Character.toUpperCase(name.charAt(0)) + name.substring(1);
return prefix + transformedName + suffix;
}
#Override
public String reverse(String transformed) {
if (transformed.startsWith(prefix)) {
String str = transformed.substring(prefix.length());
if (str.endsWith(suffix)) {
return str.substring(0, str.length() - suffix.length());
}
}
return null;
}
#Override
public String toString() { return "[FlattenNameTransformer('"+prefix+"','"+suffix+"')]"; }
}
/*===============================
* POJOS
===============================*/
public static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public static class City {
private String titleCity;
private List<Person> people;
public City(String title, List<Person> people) {
this.titleCity = title;
this.people = people;
}
public String getTitleCity() {
return titleCity;
}
public void setTitleCity(String titleCity) {
this.titleCity = titleCity;
}
#FlattenCollection
public List<Person> getPeople() {
return people;
}
public void setPeople(List<Person> people) {
this.people = people;
}
}
}
Based on this link I suspect the field-level annotation only delegates writing the value not entire properties.
A (rather kludgey) workaround might be to have a custom serializer for the entire City class:
#JsonSerialize(using = CitySerializer.class)
public class City {
private String title;
#JsonIgnore
private List<Person> people;
}
...and then
public class CitySerializer extends JsonSerializer<City> {
private static final int START_INDEX = 1;
#Override
public void serialize(City city,
JsonGenerator generator,
SerializerProvider provider) throws IOException {
generator.writeStartObject();
// Write all properties (except ignored)
JavaType javaType = provider.constructType(City.class);
BeanDescription beanDesc = provider.getConfig().introspect(javaType);
JsonSerializer<Object> serializer = BeanSerializerFactory.instance.findBeanSerializer(provider,
javaType,
beanDesc);
serializer.unwrappingSerializer(null).serialize(value, jgen, provider);`
// Custom serialization of people
List<Person> people = city.getPeople();
for (int i = 0; i < people.size(); ++i) {
Person person = people.get(i);
int index = i + START_INDEX;
serialize(person, index, generator);
}
generator.writeEndObject();
}
private void serialize(Person person, int index, JsonGenerator generator) throws IOException {
generator.writeStringField(getIndexedFieldName("personName", index),
person.getName());
generator.writeNumberField(getIndexedFieldName("personAge", index),
person.getAge());
}
private String getIndexedFieldName(String fieldName, int index) {
return fieldName + "_" + index;
}
}
I use Gson to convert JSON data to Java objects. However, the JSON structure has an extra field which could be flattened. Is this possible to do with Gson?
To elaborate (since this is rather difficult to explain), the JSON looks something like this:
{
"foo": "bar",
"data": {
"first": 0,
"second": 1,
"third": 2
}
}
This produces two classes, one for the parent and one for data, like this:
public class Entry {
private String foo;
private Data data;
}
public class Data {
private int first;
private int second;
private int third;
}
I'd like to "flatten" the data field into the parent object so that the Java class would look something like this:
public class Entry {
private String foo;
private int first;
private int second;
private int third;
}
Is this possible with Gson, using e.g. TypeAdapters?
I'll show you demo and you decide for yourself do you really want this... Because it makes TypeAdapter code hard to read.
private static class EntryTypeAdapter extends TypeAdapter<Entry> {
// without registerTypeAdapter(Entry.class, new EntryTypeAdapter())
private Gson gson = new GsonBuilder()
// ignore "foo" from deserialization and serialization
.setExclusionStrategies(new TestExclStrat()).create();
#Override
public void write(JsonWriter out, Entry value) throws IOException {
out.beginObject();
out.name("foo");
out.value(value.foo);
out.name("data");
out.value(gson.toJson(value));
out.endObject();
}
#Override
public Entry read(JsonReader in) throws IOException {
Entry entry = null;
String foo = null;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals("foo")) {
foo = in.nextString();
} else if (name.equals("data")) {
entry = gson.fromJson(in, Entry.class);
} else {
in.skipValue();
}
}
in.endObject();
if(entry!= null) entry.foo = foo;
return entry;
}
public class TestExclStrat implements ExclusionStrategy {
public boolean shouldSkipClass(Class<?> arg0) {
return false;
}
public boolean shouldSkipField(FieldAttributes f) {
return f.getName().equals("foo");
}
}
}
Can test it with this:
public static void main(String[] args) throws IOException, JSONException {
String jsonString = "{\n" +
" \"foo\": \"bar\",\n" +
" \"data\": {\n" +
" \"first\": 0,\n" +
" \"second\": 1,\n" +
" \"third\": 2\n" +
" }\n" +
"}";
Gson gson = new GsonBuilder()
.registerTypeAdapter(Entry.class, new EntryTypeAdapter()).create();
Entry el = gson.fromJson(jsonString, Entry.class);
String serialized = gson.toJson(el);
System.out.println(serialized);
}
public static class Entry {
public String foo;
public Integer first;
public Integer second;
public Integer third;
}
You could also do something like this:
// even more complicated version without inner Gson help
public Entry readOption2(JsonReader in) throws IOException {
Entry entry = new Entry();
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals("foo")) {
entry.foo = in.nextString();
} else if (name.equals("data")) {
in.beginObject();
while (in.hasNext()) {
name = in.nextName();
if (name.equals("first")) {
entry.first = in.nextInt();
} else if (name.equals("second")) {
entry.second = in.nextInt();
} else if (name.equals("third")) {
entry.third = in.nextInt();
}else{
in.skipValue();
}
}
in.endObject();
} else {
in.skipValue();
}
}
in.endObject();
return entry;
}
Eclipse can auto-generate a toString() method from a object's fields. If those fields are objects then they too may have similarly auto-generated toString() methods.
e.g. a President object might look like this:
President [country=USA, name=Name [title=Mr, forename=Barack, surname=Obama], address=Address [houseNumber=1600, street=Pennsylvania Avenue, town=Washington]]
which is easier to read if I format it:
President [
country=USA,
name=Name [
title=Mr,
forename=Barack,
surname=Obama],
address=Address [
houseNumber=1600,
street=Pennsylvania Avenue,
town=Washington]]
What is the best way to parse this String to create a map of maps?
I've got a solution, but it's not pretty. I was hoping to be able to avoid the low level String manipulation somehow, but here it is:
import java.util.LinkedHashMap;
import java.util.Map;
public class MappedObject {
public String className;
public Map<String, String> leafFields = new LinkedHashMap<>();
public Map<String, MappedObject> treeFields = new LinkedHashMap<>();
#Override
public String toString() {
return "[className=" + className
+ (leafFields.isEmpty() ? "" : ", leafFields=" + leafFields)
+ (treeFields.isEmpty() ? "" : ", treeFields=" + treeFields)
+ "]";
}
public static MappedObject createFromString(String s) {
MappedObject mo = new MappedObject();
new Mapper(s).mapObject(mo);
return mo;
}
private static class Mapper {
private String s;
public Mapper(String s) {
this.s = s;
}
private String mapObject(MappedObject mo) {
mo.className = removeFirstNCharacters(s.indexOf(' '));
while (s.contains("=")) {
removeLeadingNonLetters();
String key = removeFirstNCharacters(s.indexOf('='));
removeFirstNCharacters(1); // remove the =
String leafValue = getLeafValue();
if (leafValue != null) {
mo.leafFields.put(key, leafValue);
if (s.startsWith("]")) { // that was the last field in the tree
return s;
}
} else {
MappedObject treeField = new MappedObject();
mo.treeFields.put(key, treeField);
s = new Mapper(s).mapObject(treeField);
}
}
return s; // s contains only close brackets - ]
}
private void removeLeadingNonLetters() {
int i = 0;
while (!Character.isLetter(s.charAt(i))) {
i++;
}
removeFirstNCharacters(i);
}
private String removeFirstNCharacters(int n) {
String value = s.substring(0, n);
s = s.substring(value.length());
return value;
}
private String getLeafValue() {
int endIndex = getEndIndex();
if (!s.contains("[") || s.indexOf('[') > endIndex) {
return removeFirstNCharacters(endIndex);
}
return null;
}
/** The end of the value, if it's a leaf field. */
private int getEndIndex() {
if(s.contains(",")) {
return Math.min(s.indexOf(','), s.indexOf(']'));
}
return s.indexOf(']');
}
}
}
Hello I just want to ask how should i get the codes function into a method which is run by the main at the moment! Basically so that I can activate the working method from the main instead. I am new to programming so I am having quite a bit of trouble grasping everything properly
package bla_bla;
import java.io.IOException;
import java.util.ArrayList;
import java.util.*;
import javax.swing.text.html.parser.Element;
import org.codehaus.jackson.map.ObjectMapper;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import org.jsoup.select.Evaluator.Id;
import org.json.*;
import argo.saj.InvalidSyntaxException;
public class supermonkey {
private static ArrayList<BugsList> bugsList;
private static ArrayList<BugsList> bugbug;
public static void main(String args[]) throws IOException {
bugsList = new ArrayList<BugsList>();
bugbug = new ArrayList<BugsList>();
Document doc = Jsoup.connect("https://bugzilla.mozilla.org/rest/bug?product=Input&f1=bug_mentor&o1=isnotempty").get();
String rawData = doc.body().text();
// System.out.println(title);
JSONObject obj = new JSONObject(rawData);
// System.out.println(obj);
System.out.println(obj.get("bugs"));
JSONArray jsonMainArr = new JSONArray(obj.get("bugs").toString());
for (int i = 0; i < jsonMainArr.length(); i++) { // **line 2**
JSONObject childJSONObject = jsonMainArr.getJSONObject(i);
JSONObject assigned = childJSONObject.getJSONObject("assigned_to_detail");
// JSONObject assigned2 = childJSONObject.getJSONObject("assigned_to_detail");
int id = assigned.getInt("id");
BugsList bug = new BugsList();
BugsList bug2 = new BugsList();
bug.setId(id);
String severity = childJSONObject.getString("severity");
String resolution = childJSONObject.getString("resolution");
String summary = childJSONObject.getString("summary");
String component = childJSONObject.getString("component");
bug.setSeverity(severity);
bug.setResolution(resolution);
bug.setSummary(summary);
bug.setComponent(component);
bugsList.add(bug);
// String severity = assigned.getString();
// System.out.println("sss "+ assigned);
}
getComponent("Code Quality");
// getSeverity(524276);
// getResolution(524276);
// getSummary(524276);
}
public static void getSeverity(int id){
for(int i =0;i<bugsList.size(); i++){
if(bugsList.get(i).getId() == id){
System.out.println("The id exists in the list " + bugsList.get(i).getSeverity());
}
}
}
public static void getResolution(int id){
for(int i =0;i<bugsList.size(); i++){
if(bugsList.get(i).getId() == id){
System.out.println("The id exists in the list and The resolution is" + bugsList.get(i).getResolution());
}
}
}
public static void getSummary(int id){
for(int i =0;i<bugsList.size(); i++){
if(bugsList.get(i).getId() == id){
System.out.println("The comp.. exists in the list and The summary is " + bugsList.get(i).getSummary());
}
}
}
// Current used method
public static ArrayList<BugsList> getComponent(String component){
for(int i =0;i<bugsList.size(); i++){
if(bugsList.get(i).getComponent().equals(component)){
System.out.println("(Code Quality) component contains summary " + bugsList.get(i).getSummary() +" /The resolution is " +
bugsList.get(i).getResolution() + " /Severity is " + bugsList.get(i).getSeverity());
bugbug.add(bugsList.get(i));
}
}
return bugbug;
}
}
package bla_bla;
public class BugsList {
private String severity;
private int id;
private String resolution;
private String summary;
private String component;
public String getComponent() {
return component;
}
public void setComponent(String component) {
this.component = component;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getResolution() {
return resolution;
}
public void setResolution(String resolution) {
this.resolution = resolution;
}
public String getSeverity() {
return severity;
}
public void setSeverity(String severity) {
this.severity = severity;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
I think I understand you problem. You write your code into the main function and that not the solution.
Your main function should look something like that:
public static void main(String args[]) throws IOException {
Supermonkey supermonkey = new Supermonkey();
supermonkey.getComponent("componennt name ");
}
Your Class is Supermonkey and you create an instance call supermonkey (sometime people use mySupermonkey) . There is only one instance of this class that will use.
Then into the constructor method you can create everything to build your instance supermonkey as :
public class Supermonkey {
private ArrayList<BugsList> bugsList;
private ArrayList<BugsList> bugbug;
public Supermonkey(){
bugsList = new ArrayList<BugsList>();
bugbug = new ArrayList<BugsList>();
Document doc = Jsoup
.connect(
"https://bugzilla.mozilla.org/rest/bug?product=Input&f1=bug_mentor&o1=isnotempty")
.get();
String rawData = doc.body().text(); // ... partial code add everything about initialisation of your instance
Then you can have a method of this Class Supermonkey
// Current used method
public ArrayList<BugsList> getComponent(String component) {
for (int i = 0; i < bugsList.size(); i++) {
if (bugsList.get(i).getComponent().equals(component)) {
System.out
.println("(Code Quality) component contains summary "
+ bugsList.get(i).getSummary()
+ " /The resolution is "
+ bugsList.get(i).getResolution()
+ " /Severity is "
+ bugsList.get(i).getSeverity());
bugbug.add(bugsList.get(i));
}
}
return bugbug;
}
A this method is call from the public static void main(String args[]) with supermonkey.getComponent("componennt name ");
Also I think about to change the name of Class BugsList it seems to be a BugItem , may be ?
I am working on an Android application, using the EmpireAvenue API.
The API uses JSON and I'm using the GSON library to parse the data from the API.
Here is the problem:
I have a JSON structure like this:
{
type: "earnings",
info: {
earnings: 64.09
dividends: 1277.34
gains: 1997.05
expenses: 4895.51
shares_bought: 210
shares_bought_user_count: 2
shares_sold: 0
shares_sold_user_count: 0
},
created: "2011-04-16 11:32:37"
},
{
type: "following",
info: [
{
ticker: "SOLPHE"
full_name: "Rodrigo Bermudez Salazar"
list_name: "My Recommended Buys"
},
{
ticker: "SOLPHE"
full_name: "Rodrigo Bermudez Salazar"
list_name: "My Watch List"
}
],
created: "2011-04-16 11:00:08"
}
As you can see, the structure associated with the info field is different. Sometimes it's an object, sometimes an array. As expected, the GSON library throws errors when parsing.
Do you know how to parse a JSON structure with when a field changes structure ?
Thanks for your help.
The current solution with Gson is a bit involved, requiring implementation of a custom Instance Creator and/or a custom Deserializer. Take a look at http://code.google.com/p/google-gson/issues/detail?id=231 and the release notes on Hierarchical Type Adapters for details. I just posted an example of polymorphic deserialization with Gson in response to Polymorphism with gson.
Gson hopefully will soon have the RuntimeTypeAdapter for simpler polymorphic deserialization. See http://code.google.com/p/google-gson/issues/detail?id=231 for more info.
On the other hand, a Jackson-based solution isn't so bad.
public class Foo
{
static String jsonInput =
"[" +
"{" +
"\"type\":\"earnings\"," +
"\"info\":" +
"{" +
"\"earnings\":64.09," +
"\"dividends\":1277.34," +
"\"gains\":1997.05," +
"\"expenses\":4895.51," +
"\"shares_bought\":210," +
"\"shares_bought_user_count\":2," +
"\"shares_sold\":0," +
"\"shares_sold_user_count\":0" +
"}," +
"\"created\":\"2011-04-16 11:32:37\"" +
"}," +
"{" +
"\"type\":\"following\"," +
"\"info\":" +
"[" +
"{" +
"\"ticker\":\"SOLPHE\"," +
"\"full_name\":\"RodrigoBermudezSalazar\"," +
"\"list_name\":\"MyRecommendedBuys\"" +
"}," +
"{" +
"\"ticker\":\"SOLPHE\"," +
"\"full_name\":\"RodrigoBermudezSalazar\"," +
"\"list_name\":\"MyWatchList\"" +
"}" +
"]," +
"\"created\":\"2011-04-16 11:00:08\"" +
"}" +
"]";
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new CamelCaseNamingStrategy());
DateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
mapper.setDateFormat(dataFormat);
Collection<Thing> things = mapper.readValue(jsonInput, new TypeReference<Collection<Thing>>(){});
System.out.println(things);
}
}
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
#JsonSubTypes({#Type(value=Earnings.class, name="earnings"), #Type(value=Following.class, name="following")})
abstract class Thing
{
private Date created;
void setCreated(Date created)
{
this.created = created;
}
#Override
public String toString()
{
return String.format(
"[%1$s: created=%2$s, other attributes:%3$s]",
getClass().getSimpleName(), created, toStringAddenda());
}
abstract String toStringAddenda();
}
class Earnings extends Thing
{
private EarningsInfo info;
void setInfo(EarningsInfo info)
{
this.info = info;
}
#Override
String toStringAddenda()
{
return info.toString();
}
}
class Following extends Thing
{
private Collection<FollowingInfo> info;
void setInfo(Collection<FollowingInfo> info)
{
this.info = info;
}
#Override
String toStringAddenda()
{
return info.toString();
}
}
class FollowingInfo
{
private String ticker;
private String fullName;
private String listName;
void setTicker(String ticker)
{
this.ticker = ticker;
}
void setFullName(String fullName)
{
this.fullName = fullName;
}
void setListName(String listName)
{
this.listName = listName;
}
#Override
public String toString()
{
return String.format(
"[FollowingInfo: ticker=%1$s, fullName=%2$s, listName=%3$s]",
ticker, fullName, listName);
}
}
class EarningsInfo
{
private BigDecimal earnings;
private BigDecimal dividends;
private BigDecimal gains;
private BigDecimal expenses;
private int sharesBought;
private int sharesBoughtUserCount;
private int sharesSold;
private int sharesSoldUserCount;
void setEarnings(BigDecimal earnings)
{
this.earnings = earnings;
}
void setDividends(BigDecimal dividends)
{
this.dividends = dividends;
}
void setGains(BigDecimal gains)
{
this.gains = gains;
}
void setExpenses(BigDecimal expenses)
{
this.expenses = expenses;
}
void setSharesBought(int sharesBought)
{
this.sharesBought = sharesBought;
}
void setSharesBoughtUserCount(int sharesBoughtUserCount)
{
this.sharesBoughtUserCount = sharesBoughtUserCount;
}
void setSharesSold(int sharesSold)
{
this.sharesSold = sharesSold;
}
void setSharesSoldUserCount(int sharesSoldUserCount)
{
this.sharesSoldUserCount = sharesSoldUserCount;
}
#Override
public String toString()
{
return String.format(
"[EarningsInfo: earnings=%1$s, dividends=%2$s, gains=%3$s, expenses=%4$s, sharesBought=%5$s, sharesBoughtUserCount=%6$s, sharesSold=%7$s, sharesSoldUserCount=%8$s]",
earnings, dividends, gains, expenses, sharesBought, sharesBoughtUserCount, sharesSold, sharesSoldUserCount);
}
}
class CamelCaseNamingStrategy extends PropertyNamingStrategy
{
#Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName)
{
return convert(defaultName);
}
#Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName)
{
return convert(defaultName);
}
#Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName)
{
return convert(defaultName);
}
private String convert(String defaultName)
{
char[] nameChars = defaultName.toCharArray();
StringBuilder nameTranslated = new StringBuilder(nameChars.length * 2);
for (char c : nameChars)
{
if (Character.isUpperCase(c))
{
nameTranslated.append("_");
c = Character.toLowerCase(c);
}
nameTranslated.append(c);
}
return nameTranslated.toString();
}
}