Getting issue while parsing json using jackson - json

i am using jackson to parse data i am using following code to parse json
public Map<String, Object> savePreference(#RequestBody Map map) throws IOException{
List preferenceDetails = (List) map.get("data");
int preferenceIndex = 0;
while(preferenceIndex < preferenceDetails.size()){
final ObjectMapper mapper = new ObjectMapper();
System.out.println(preferenceDetails.get(preferenceIndex));
mapper.readValue(preferenceDetails.get(preferenceIndex).toString(), Preference.class);
preferenceIndex++;
}
return null;
}
i am sending json from client side like this
{"data":[
{
"preferenceType":"Travelling"
},
{
"preferenceType":"Shopping"
}
]
}
but above code throws exception when i called
mapper.readValue(preferenceDetails.get(preferenceIndex).toString(), Preference.class);
exception is
com.fasterxml.jackson.core.JsonParseException: Unexpected character ('p' (code 112)): was expecting double-quote to start field name
i am printing preference details in while loop
{preferenceType=Travelling}

You do not need to parse line by line. Using objects directly will be much more easier. Here is a test case which demonstrates how to parse your json to a object directly.
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import java.util.Collection;
public class JacksonTest {
#Test
public void testName() throws Exception {
final String test = "{\"data\":[\n" +
" {\n" +
" \"preferenceType\":\"Travelling\"\n" +
" },\n" +
" {\n" +
" \"preferenceType\":\"Shopping\"\n" +
" }\n" +
" ]\n" +
"}";
final ObjectMapper objectMapper = new ObjectMapper();
final Data data = objectMapper.readValue(test, Data.class);
Assert.assertNotNull(data);
Assert.assertEquals(2, data.getData().size());
}
static class Data {
private Collection<PreferenceType> data;
public Collection<PreferenceType> getData() {
return data;
}
public void setData(Collection<PreferenceType> data) {
this.data = data;
}
}
static class PreferenceType {
private String preferenceType;
public String getPreferenceType() {
return preferenceType;
}
public void setPreferenceType(String preferenceType) {
this.preferenceType = preferenceType;
}
}
}
After that you can build your own map how ever you want. But your framework may be able to handle this kind of request. You do not need to parse it manually. You should try getting Data object directly, instead of getting your body as Map.
Update
Try something like this. But first define Data class properly ( at least not as an inner class).
public Map<String, Object> savePreference(#RequestBody Data data) throws IOException{
...
}
Update 2
And also your map already has your object PreferenceDetail. It may already handle the json and map it as object. Please debug and check if your map.get("data") returns List<PreferenceDetail> or not. If it returns List there is no more work is needed to parse json using jackson. You can simply do something like this.
public Map<String, Object> savePreference(#RequestBody Map map) throws IOException{
List<PreferenceDetail> preferenceDetails = (List) map.get("data");
for (PreferenceDetail preferenceDetail : preferenceDetails) {
System.out.println(preferenceDetail.getPreferenceType());
}
return null;
}

I solved my problem by using jackson object mapper.
public Map<String, Object> savePreference(#RequestBody Map map) throws IOException{
log.debug("saving preferences");
if(preferenceService.getPreferencesByUser() != null && preferenceService.getPreferencesByUser().size() != 0)
return ResponseHandler.generateResponse(configProp.getProperty("user.preference.exist"), HttpStatus.ACCEPTED, true, null);
final ObjectMapper mapper = new ObjectMapper();
List preferenceDetails = (List) map.get("data");
int preferenceIndex = 0;
while(preferenceIndex < preferenceDetails.size()){
preferenceService.savePreference(mapper.readValue(mapper.writeValueAsString(preferenceDetails.get(preferenceIndex)), Preference.class));
preferenceIndex++;
}
return ResponseHandler.generateResponse(configProp.getProperty("preference.added"), HttpStatus.ACCEPTED, true, null);
}

Related

How to convert a complex object, having a HashMap parameter, into JSON and back? [duplicate]

I get the error:
Exception in thread "main" com.google.gson.JsonParseException:
Expecting object found: "com.shagie.app.SimpleMap$Data#24a37368"
when trying to deseralize a Map that uses non-trivial keys:
package com.shagie.app;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.HashMap;
public class SimpleMap {
public static void main(String[] args) {
Wrapper w = new Wrapper();
w.m.put(new Data("f", 1), new Data("foo", 3));
w.m.put(new Data("b", 2), new Data("bar", 4));
GsonBuilder gb = new GsonBuilder();
gb.setPrettyPrinting();
Gson g = gb.create();
String json = g.toJson(w);
System.out.println(json);
w = g.fromJson(json, Wrapper.class);
System.out.println(w.m.isEmpty());
}
static public class Wrapper {
HashMap<Data, Data> m = new HashMap<Data, Data>();
}
static public class Data {
String s;
Integer i;
public Data(String arg, Integer val) { s = arg; i = val; }
}
}
This serializes to the json:
{
"m": {
"com.shagie.app.SimpleMap$Data#24a37368": {
"s": "foo",
"i": 3
},
"com.shagie.app.SimpleMap$Data#66edc3a2": {
"s": "bar",
"i": 4
}
}
}
One can see the key attempting to be serialized, but certainly not in a way that can be deserialized.
How does one serialize this object so that it can be deserialized?
I found the following while trying to solve this puzzle: Issue 210: Cannot serialize or deserialize Maps with complex keys.
For any internet travelers from the future (like myself)... you can enable this functionality in GSON 2.* with the enableComplexMapKeySerialization() method on GsonBuilder.
Here's the javadoc for that method.
When enabled, the map will be serialized (and correctly deserialized) as an array of [key, value] arrays:
{"m":[[{"s":"f", "i",1}, {"s":"foo", "i":3}], [{"s":"b", "i",2}, {"s":"bar", "i":4}]]}
The problem is that toString() is getting called on the keys to the map, rather than them being serialized themselves.
To fix this a custom serializer and deserializer needs to be set up, and the deserializer needs to be aware of the format that the object uses to display itself as a string (the toString() method must return a string that can be used to reconstruct the entire object).
For the above example:
package com.shagie.app;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.util.HashMap;
public class SimpleMapFixed {
public static void main(String[] args) {
Wrapper w = new Wrapper();
w.m.put(new Data("f", 1), new Data("foo", 3));
w.m.put(new Data("b", 2), new Data("bar", 4));
GsonBuilder gb = new GsonBuilder();
gb.setPrettyPrinting();
gb.registerTypeAdapter(Data.class, new DataSerializer());
Gson g = gb.create();
String json = g.toJson(w);
System.out.println(json);
w = g.fromJson(json, Wrapper.class);
System.out.println(w.m.isEmpty());
}
static public class Wrapper {
HashMap<Data, Data> m = new HashMap<Data, Data>();
}
static public class DataSerializer implements JsonSerializer<Data>,
JsonDeserializer<Data> {
#Override
public Data deserialize(JsonElement je, Type t, JsonDeserializationContext ctx)
throws JsonParseException {
Data rv;
JsonObject jo;
System.out.println("deserialize called with: " + je.toString());
if (je.isJsonObject()) {
jo = je.getAsJsonObject();
rv = new Data(jo.get("s").getAsString(), jo.get("i").getAsInt());
} else {
String js = je.getAsString();
String[] s = js.split(":", 2); // split into two (and only two)
rv = new Data(s[1], Integer.valueOf(s[0]));
}
System.out.println("deserialize returns: " + rv.s + " " + rv.i);
return rv;
}
#Override
public JsonElement serialize(Data data, Type type, JsonSerializationContext jsonSerializationContext) {
JsonObject jo = new JsonObject();
jo.addProperty("s", data.s);
jo.addProperty("i", data.i);
System.out.println("serialize called: " + jo.toString());
return jo;
}
}
static public class Data {
String s;
Integer i;
public Data(String arg, Integer val) { s = arg; i = val; }
#Override
public String toString() {
String rv = i.toString() + ':' + s;
System.out.println("toString called: " + rv);
return rv;
}
}
}
Running this code produces:
serialize called: {"s":"foo","i":3}
toString called: 1:f
serialize called: {"s":"bar","i":4}
toString called: 2:b
{
"m": {
"1:f": {
"s": "foo",
"i": 3
},
"2:b": {
"s": "bar",
"i": 4
}
}
}
deserialize called with: "1:f"
deserialize returns: f 1
deserialize called with: {"s":"foo","i":3}
deserialize returns: foo 3
deserialize called with: "2:b"
deserialize returns: b 2
deserialize called with: {"s":"bar","i":4}
deserialize returns: bar 4
Note the invocations of toString() as part of the serialization. In this code, the logic for the deserializion from the String form is in the DataSerializer, though it may make sense to move it into the Data class as another constructor instead - it doesn't affect the final outcome.
Further note that Data was a rather simple object itself with no deeper structures. Trying to serialize that as the key would require additional work.
Its Up to you how you are maintaining the HahMap Keys, You can deserialized it with simple and easiest way.
final Type typeOf = new TypeToken <Map<String, Map<String, Data>>>(){}.getType();
final Map<String, Map<String, Data>> newMap = gson.fromJson(json, typeOf);
final Map<String, Data> map = newMap.get("m");
final Iterator<Entry<String, Data>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String,Data> pair = (Map.Entry<String,Data>) it.next();
String key = pair.getKey();
System.out.println("key "+ key + " Values[ i= " + data.getI() + ", s= " +data.getS()+" ]");
}
Result:
key = snippet.Snippet$Data#61506150 Values [ i= 3, s= foo ]
key = snippet.Snippet$Data#63ff63ff Values [ i= 4, s= bar ]

Spring boot rest mvc json parser is not validating input json data which is illegal or has trailing data

In my rest client i am passing the below JSON request data:
{
"jobName":"test1",
"source":{ "name":"prod1","type":"teradata"},
"target":{ "name":"prod2","type":"teradata"},
"objects":{ "name":"table1"}<br/>
}
junkdata ; ##%$##%
So the extra "junkdata ; ##%$##%" not got validated by the rest client or by the spring jackson out-of-the box message converter.
I did debug the code, the spring HttpServletRequest body has the complete data including the junk data. As such its not failing, the spring is ignoring the junk data and converting the starting JSON data into Java object.
I did try by adding annotations like #JsonFormat for #RequestBody in rest controller calls (#RestController). But its not validating seems the Spring out-of-the box message converter jackson is not validating the incoming request JSON data properly.
Now this issue (failing on trailing tokens or data) is fixed in the spring jackson https://github.com/FasterXML/jackson-databind/issues/1583
using DeserializationFeature.FAIL_ON_TRAILING_TOKENS, Here is the code sample to fix the issue:
#Configuration
public class RestControllerConfiguration extends WebMvcConfigurationSupport
{
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
}
}
After working on different ways i got the solution using Google gson.jar, #Pete yes i have validate the JSON input which is invalid.
The google gson api is validating it properly, we need to use the custom message converter to validate it in the rest WebMvcConfigurationSupport class.
#Configuration
#ComponentScan(basePackages = { "com.teradata.datamovement.rest.controllers",
"com.teradata.rest.controller" })
public class RestControllerConfiguration extends WebMvcConfigurationSupport
{
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
log.debug("Adding custom message converter.");
converters.add(new AbstractHttpMessageConverter<Object>(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")){
#Override
protected Object readInternal(Class<? extends Object> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
try{
log.debug("Converting and validating the http request body data.");
String httpRequestBody = convertStreamToString(inputMessage.getBody());
log.debug("Http request body data:"+httpRequestBody);
return new Gson().fromJson(httpRequestBody, clazz);
}
catch(JsonSyntaxException e){
throw new HttpMessageNotReadableException("Invalid input JSON data: " + e.getMessage(), e);
}
}
private String convertStreamToString(InputStream is) throws IOException {
if (is != null) {
Writer writer = new StringWriter();
char[] buffer = new char[1024];
try {
Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
int n;
while ((n = reader.read(buffer)) != -1) {
writer.write(buffer, 0, n);
}
} finally {
is.close();
}
return writer.toString();
} else {
return "";
}
}
#Override
protected boolean supports(Class clazz) {
return true;
}
#Override
protected void writeInternal(Object t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
outputMessage.getBody().write(new Gson().toJson(t).getBytes());
}
});
}
}
But the weird thing i have noticed is that its working only if i make it as anonymous class or by adding the class with in the same file. If i create this custom message converter out side this RestControllerConfiguration.java file, then its not validating it.
Here is the example:
{
"jobName":"test1",
"source":{ "name":"prod1","type":"teradata"},
"target":{ "name":"prod2","type":"teradata"},
"objects":{ "name":"table1"}
}
junkdata ; ##%$##%
This will get validated, and will throw error like
{"message":"Invalid input JSON data: com.google.gson.stream.MalformedJsonException: Expected EOF at line 7 column 1; nested exception is com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Expected EOF at line 7 column 1"}

Handle JSON which sends array of items but sometimes empty string in case of 0 elements

I have a JSON which sends array of element in normal cases but sends empty string "" tag without array [] brackets in case of 0 elements.
How to handle this with Gson? I want to ignore the error and not cause JSONParsingException.
eg.
"types": [
"Environment",
"Management",
"Computers"
],
sometimes it returns:
"types" : ""
Getting the following exception: Expected BEGIN ARRAY but was string
Since you don't have control over the input JSON string, you can test the content and decide what to do with it.
Here is an example of a working Java class:
import com.google.gson.Gson;
import java.util.ArrayList;
public class Test {
class Types {
Object types;
}
public void test(String input) {
Gson gson = new Gson();
Types types = gson.fromJson(input,Types.class);
if(types.types instanceof ArrayList) {
System.out.println("types is an ArrayList");
} else if (types.types instanceof String) {
System.out.println("types is an empty String");
}
}
public static void main(String[] args) {
String input = "{\"types\": [\n" +
" \"Environment\",\n" +
" \"Management\",\n" +
" \"Computers\"\n" +
" ]}";
String input2 = "{\"types\" : \"\"}";
Test testing = new Test();
testing.test(input2); //change input2 to input
}
}
If a bad JSON schema is not under your control, you can implement a specific type adapter that would try to determine whether the given JSON document is fine for you and, if possible, make some transformations. I would recomment to use #JsonAdapter in order to specify improperly designed types (at least I hope the entire API is not improperly designed).
For example,
final class Wrapper {
#JsonAdapter(LenientListTypeAdapterFactory.class)
final List<String> types = null;
}
where LenientListTypeAdapterFactory can be implemented as follows:
final class LenientListTypeAdapterFactory
implements TypeAdapterFactory {
// Gson can instantiate it itself, let it just do it
private LenientListTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Obtaining the original list type adapter
#SuppressWarnings("unchecked")
final TypeAdapter<List<?>> realListTypeAdapter = (TypeAdapter<List<?>>) gson.getAdapter(typeToken);
// And wrap it up in the lenient JSON type adapter
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) new LenientListTypeAdapter(realListTypeAdapter);
return castTypeAdapter;
}
private static final class LenientListTypeAdapter
extends TypeAdapter<List<?>> {
private final TypeAdapter<List<?>> realListTypeAdapter;
private LenientListTypeAdapter(final TypeAdapter<List<?>> realListTypeAdapter) {
this.realListTypeAdapter = realListTypeAdapter;
}
#Override
public void write(final JsonWriter out, final List<?> value)
throws IOException {
realListTypeAdapter.write(out, value);
}
#Override
public List<?> read(final JsonReader in)
throws IOException {
// Check the next (effectively current) JSON token
switch ( in.peek() ) {
// If it's either `[...` or `null` -- we're supposing it's a "normal" list
case BEGIN_ARRAY:
case NULL:
return realListTypeAdapter.read(in);
// Is it a string?
case STRING:
// Skip the value entirely
in.skipValue();
// And return a new array list.
// Note that you might return emptyList() but Gson uses mutable lists so we do either
return new ArrayList<>();
// Not anything known else?
case END_ARRAY:
case BEGIN_OBJECT:
case END_OBJECT:
case NAME:
case NUMBER:
case BOOLEAN:
case END_DOCUMENT:
// Something definitely unexpected
throw new MalformedJsonException("Cannot parse " + in);
default:
// This would never happen unless Gson adds a new type token
throw new AssertionError();
}
}
}
}
Here is it how it can be tested:
for ( final String name : ImmutableList.of("3-elements.json", "0-elements.json") ) {
try ( final Reader reader = getPackageResourceReader(Q43562427.class, name) ) {
final Wrapper wrapper = gson.fromJson(reader, Wrapper.class);
System.out.println(wrapper.types);
}
}
Output:
[Environment, Management, Computers]
[]
If the entire API uses "" for empty arrays, then you can drop the #JsonAdapter annotation and register the LenientListTypeAdapterFactory via GsonBuilder, but add the following lines to the create method in order not to break other type adapters:
if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
// This tells Gson to try to pick up the next best-match type adapter
return null;
}
...
There are a lot of weirdly designed JSON response choices, but this one hits the top #1 issue where nulls or empties are represented with "". Good luck!
Thanks for all your answers.
The recommed way as mentioned in above answers would be to use TypeAdapters and ExclusionStrategy for GSON.
Here is a good example Custom GSON desrialization

gson flat json to nested objects needs serializer/deserializer?

I have some JSON coming in (I don't have any control or ability to change the structure and/or naming within the JSON...important to keep in mind in this question) that has a "flat" structure similar to this:
{
"name": "...",
"email": "...",
"box_background_color": "...",
"box_border_color": "...",
"box_text_color": "...",
...
}
Now, I can just create a simple object that keeps everything flat, like so:
public class Settings {
#SerializedName("name")
private String _name;
#SerializedName("email")
private String _emailAddress;
#SerializedName("box_background_color")
private String _boxBackgroundColor;
#SerializedName("box_border_color")
private String _boxBorderColor;
#SerializedName("box_text_color")
private String _boxTextColor;
...
}
However, I want everything associated with box settings to be in it's own class (BoxSettings). This is more like what I want:
public class Settings {
#SerializedName("name")
private String _name;
#SerializedName("email")
private String _emailAddress;
private BoxSettings _boxSettings
...
}
public class BoxSettings {
#SerializedName("box_background_color")
private String _boxBackgroundColor;
#SerializedName("box_border_color")
private String _boxBorderColor;
#SerializedName("box_text_color")
private String _boxTextColor;
...
}
I know that if the JSON was structured such that the box settings were nested then it would be easy to accomplish what I want, however, I don't have the ability to change the structure of the JSON, so please don't suggest that (I would do it if I could).
My question is this: Is creating an entire TypeAdapter the only way to accomplish what I want or can I still accomplish most of this with annotations? If it is not the only way, how else can I accomplish this without changing the JSON at all?
The following is an example of what I mean by "creating an entire TypeAdapter":
public class SettingsTypeAdapter implements JsonDeserializer<Settings>, JsonSerializer<Settings> {
#Override
public JsonElement serialize(Settings src, Type typeOfSrc, JsonSerializationContext context) {
// Add _name
// Add _emailAddress
// Add BoxSettings._boxBackgroundColor
// Add BoxSettings._boxBorderColor
// Add BoxSettings._boxTextColor
return jsonElement;
}
#Override
public Settings deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// Read _name
// Read _emailAddress
// Read BoxSettings._boxBackgroundColor
// Read BoxSettings._boxBorderColor
// Read BoxSettings._boxTextColor
return settings;
}
}
The TypeAdapter is not the only way, but in this case would be the best way since you can associate the adapter with a Gson instance (or whatever library you are using) and have all your mapping code there.
Another way is to use JAVA reflection. I've used a version of the below code in my projects before but never with JSON and never with nested objects (mostly when there was no other choice or if i wanted to map a SQL result set to a Java object without calling resultSet.get... a lot of times).
This will work in this case.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.json.JSONObject;
public class Main {
public static void main(String[] args) {
try {
String json = "{\"name\": \"test name\", \"email\": \"email#email.com\", \"box_background_color\": \"red\", \"box_border_color\": \"orange\", \"box_text_color\": \"white\", \"test3_var2\":3}";
JSONObject jsonObject = new JSONObject(json);
System.out.println(jsonObject);
System.out.println();
/*
* need to parse JSON into a map of String, Object
*/
Map<String, Object> mapAll = new HashMap<String, Object>();
Iterator<String> iter = jsonObject.keys();
while (iter.hasNext()) {
String key = (String) iter.next();
Object value = jsonObject.get(key);
mapAll.put(key, value);
System.out.println(key + "::::" + value);
}
System.out.println();
/*
* use the mapper to generate the objects
*/
MyMapper<TestClass1> myMapper = new MyMapper<TestClass1>();
TestClass1 result = myMapper.mapToObject(mapAll, TestClass1.class);
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyMapper<T> {
#SuppressWarnings("unchecked")
public T mapToObject(Map<String, Object> flatStructure, Class<T> objectClass) {
T result = null;
Field[] fields = null;
try {
// new base object
result = objectClass.newInstance();
// get all of its fields
fields = objectClass.getDeclaredFields();
for (Field field : fields) {
// normal variable
if (field.isAnnotationPresent(MyColumn.class)) {
String variableKey = field.getAnnotation(MyColumn.class).variableKey();
setJavaFieldValue(result, field.getName(), flatStructure.get(variableKey));
}
// variable that is an object and itself has to be mapped
else if (field.isAnnotationPresent(MyInnerColumn.class)) {
String startsWith = field.getAnnotation(MyInnerColumn.class).startsWith();
// reduce the map to only have attributes that are related to this field
Map<String, Object> reducedMap = reduceMap(startsWith, flatStructure);
// make sure that there are attributes for the inner object
if (reducedMap != null) {
// map the inner object
MyMapper<T> myMapper = new MyMapper<T>();
T t2 = myMapper.mapToObject(reducedMap, (Class<T>) field.getType());
// set the mapped object to the base objecct
setJavaFieldValue(result, field.getName(), t2);
}
} else {
// no annotation on the field so ignored
}
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
private Map<String, Object> reduceMap(String startsWith, Map<String, Object> mapToReduce) {
Map<String, Object> result = new HashMap<String, Object>();
for (Map.Entry<String, Object> entry : mapToReduce.entrySet()) {
if (entry.getKey().toLowerCase().startsWith(startsWith.toLowerCase())) {
result.put(entry.getKey(), entry.getValue());
}
}
return result.size() == 0 ? null : result;
}
private void setJavaFieldValue(Object object, String fieldName, Object fieldValue) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
boolean fieldAccess = field.isAccessible();
// make the field accessible
field.setAccessible(true);
field.set(object, fieldValue);
// put it back to the way it was
field.setAccessible(fieldAccess);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
* Annotation for a regular variable / field
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface MyColumn {
// the variable's JSON key
String variableKey() default "";
}
/*
* Annotation for an inner / nested variable / field
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface MyInnerColumn {
/*
* JSON keys that start with this string will be
* associated with this nested field
*/
String startsWith() default "";
}
class TestClass1 {
#MyColumn(variableKey = "name")
private String _name;
#MyColumn(variableKey = "email")
private String _emailAddress;
#MyInnerColumn(startsWith = "box_")
private TestClass2 innerClass;
#MyInnerColumn(startsWith = "test3_")
private TestClass3 innerClass2;
#Override
public String toString() {
return "TestClass1 [_name=" + _name + ", _emailAddress=" + _emailAddress + ", innerClass=" + innerClass + ", innerClass2=" + innerClass2 + "]";
}
}
class TestClass2 {
#MyColumn(variableKey = "box_background_color")
private String _boxBackgroundColor;
#MyColumn(variableKey = "box_border_color")
private String _boxBorderColor;
#MyColumn(variableKey = "box_text_color")
private String _boxTextColor;
#Override
public String toString() {
return "TestClass2 [_boxBackgroundColor=" + _boxBackgroundColor + ", _boxBorderColor=" + _boxBorderColor
+ ", _boxTextColor=" + _boxTextColor + "]";
}
}
class TestClass3 {
#MyColumn(variableKey = "test3_var1")
private String _test3Var1;
#MyColumn(variableKey = "test3_var2")
private int _test3Var2;
#Override
public String toString() {
return "TestClass3 [_test3Var1=" + _test3Var1 + ", _test3Var2=" + _test3Var2 + "]";
}
}
Output
{"box_background_color":"red","box_text_color":"white","test3_var2":3,"name":"test name","email":"email#email.com","box_border_color":"orange"}
box_background_color::::red
box_text_color::::white
test3_var2::::3
name::::test name
email::::email#email.com
box_border_color::::orange
TestClass1 [_name=test name, _emailAddress=email#email.com, innerClass=TestClass2 [_boxBackgroundColor=red, _boxBorderColor=orange, _boxTextColor=white], innerClass2=TestClass3 [_test3Var1=null, _test3Var2=3]]

How to serialize such a custom type to json with google-gson?

First, I have a very simple java bean which can be easily serialized to json:
class Node {
private String text;
// getter and setter
}
Node node = new Node();
node.setText("Hello");
String json = new Gson().toJson(node);
// json is { text: "Hello" }
Then in order to make such beans have some dynamic values, so I create a "WithData" base class:
Class WithData {
private Map<String, Object> map = new HashMap<String, Object>();
public void setData(String key, Object value) { map.put(key, value); }
public Object getData(String key) = { return map.get(key); }
}
class Node extends WithData {
private String text;
// getter and setter
}
Now I can set more data to a node:
Node node = new Node();
node.setText("Hello");
node.setData("to", "The world");
But Gson will ignore the "to", the result is still { text: "Hello" }. I expect it to be: { text: "Hello", to: "The world" }
Is there any way to write a serializer for type WithData, that all classes extend it will not only generate its own properties to json, but also the data in the map?
I tried to implement a custom serializer, but failed, because I don't know how to let Gson serialize the properties first, then the data in map.
What I do now is creating a custom serializer:
public static class NodeSerializer implements JsonSerializer<Node> {
public JsonElement serialize(Node src,
Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("id", src.id);
obj.addProperty("text", src.text);
obj.addProperty("leaf", src.leaf);
obj.addProperty("level", src.level);
obj.addProperty("parentId", src.parentId);
obj.addProperty("order", src.order);
Set<String> keys = src.getDataKeys();
if (keys != null) {
for (String key : keys) {
obj.add(key, context.serialize(src.getData(key)));
}
}
return obj;
};
}
Then use GsonBuilder to convert it:
Gson gson = new GsonBuilder().
registerTypeAdapter(Node.class, new NodeSerializer()).create();
Tree tree = new Tree();
tree.addNode(node1);
tree.addNode(node2);
gson.toJson(tree);
Then the nodes in the tree will be converted as I expected. The only boring thing is that I need to create a special Gson each time.
Actually, you should expect Node:WithData to serialize as
{
"text": "Hello",
"map": {
"to": "the world"
}
}
(that's with "pretty print" turned on)
I was able to get that serialization when I tried your example. Here is my exact code
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;
public class Class1 {
public static void main(String[] args) throws MalformedURLException {
GsonBuilder gb = new GsonBuilder();
Gson g = gb.setPrettyPrinting().create();
Node n = new Node();
n.setText("Hello");
n.setData("to", "the world");
System.out.println(g.toJson(n));
}
private static class WithData {
private Map<String, Object> map = new HashMap<String, Object>();
public void setData(String key, Object value) { map.put(key, value); }
public Object getData(String key) { return map.get(key); }
}
private static class Node extends WithData {
private String text;
public Node() { }
public String getText() {return text;}
public void setText(String text) {this.text = text;}
}
}
I was using the JDK (javac) to compile - that is important because other compilers (those included with some IDEs) may remove the information on which Gson relies as part of their optimization or obfuscation process.
Here are the compilation and execution commands I used:
"C:\Program Files\Java\jdk1.6.0_24\bin\javac.exe" -classpath gson-2.0.jar Class1.java
"C:\Program Files\Java\jdk1.6.0_24\bin\java.exe" -classpath .;gson-2.0.jar Class1
For the purposes of this test, I put the Gson jar file in the same folder as the test class file.
Note that I'm using Gson 2.0; 1.x may behave differently.
Your JDK may be installed in a different location than mine, so if you use those commands, be sure to adjust the path to your JDK as appropriate.