I'm trying to write a general Gson serializer/deserializer for java.javax.JsonObjects:
public static class JavaxJsonObjConverter implements JsonSerializer<JsonObject>, JsonDeserializer<JsonObject> {
#Override
public JsonObject deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return JsonUtils.getJsonObjectFromString(json.toString());
}
#Override
public JsonElement serialize(JsonObject src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonParser().parse(src.toString());
}
}
When I try to serialize a java.json.JsonObject, I get this error:
Exception in thread "main" java.lang.ClassCastException: org.glassfish.json.JsonStringImpl cannot be cast to javax.json.JsonObject
at om.headlandstech.utils.gson_utils.GsonUtils$JavaxJsonValueConverter.serialize(>GsonUtils.java:1)
at com.google.gson.internal.bind.TreeTypeAdapter.write(TreeTypeAdapter.java:81)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapte>rFactory.java:208)
at ....
It would be much better if you'd post the javax.json.JsonObject instance as well (they way it's built). Because: the closest I can reproduce it with is the following:
final Gson gson = new GsonBuilder()
.registerTypeAdapter(JsonObject.class, new JavaxJsonObjConverter())
.create();
final JsonObject src = Json.createObjectBuilder()
.add("foo", "bar")
.build();
System.out.println(gson.toJson(src.get("foo"), JsonObject.class));
Exception:
Exception in thread "main" java.lang.ClassCastException: org.glassfish.json.JsonStringImpl cannot be cast to javax.json.JsonObject
at q43376802.Q43376802$JavaxJsonObjConverter.serialize(Q43376802.java:29)
at com.google.gson.internal.bind.TreeTypeAdapter.write(TreeTypeAdapter.java:81)
at com.google.gson.Gson.toJson(Gson.java:669)
at com.google.gson.Gson.toJson(Gson.java:648)
at com.google.gson.Gson.toJson(Gson.java:603)
at q43376802.Q43376802.main(Q43376802.java:26)
Next thing. Your JavaxJsonObjConverter implements a javax.json.JavaObject (de)serializer, but javax.json.JavaObject is not the root of JSON objects in javax.json. The hierarchy root is JsonValue. So your (de)serializer must deal with JsonValue rather than JsonObject.
public static class JavaxJsonValConverter
implements JsonSerializer<JsonValue> {
#Override
public JsonElement serialize(final JsonValue jsonValue, final Type type, final JsonSerializationContext context) {
return new JsonParser().parse(jsonValue.toString());
}
}
And register it with removing and deleting JavaxJsonObjConverter entirely:
.registerTypeAdapter(JsonValue.class, new JavaxJsonValConverter())
However, the serializer above is naive and requires more resources however, giving you some flexibility (when reading/writing directly from/to JSON streams may be too unjustified (compare DOM and SAX in XML -- it's the same story)):
JsonSerializer and JsonDeserializer rely on JSON tree model representation that's implemented with JsonElement. This means that entire JSON has to be loaded into memory and its tree model has to be built before you can use it. This would consume much more memory if JSON objects you're going to deal with are large.
toString() is a bad choice either: it requires internal strings to be generated first, thus consuming memory again.
So, the items above may make a really large memory print. In order to save memory resources, you can create a Gson TypeAdapter that can work with JSON streams (and that is the base for every (de)serializer in JSON).
final class JsonValueTypeAdapter
extends TypeAdapter<JsonValue> {
private static final TypeAdapter<JsonValue> jsonValueTypeAdapter = new JsonValueTypeAdapter();
private JsonValueTypeAdapter() {
}
static TypeAdapter<JsonValue> getJsonValueTypeAdapter() {
return jsonValueTypeAdapter;
}
#Override
public void write(final JsonWriter out, final JsonValue jsonValue)
throws IOException {
final ValueType valueType = jsonValue.getValueType();
switch ( valueType ) {
case ARRAY:
JsonArrayTypeAdapter.instance.write(out, (JsonArray) jsonValue);
break;
case OBJECT:
JsonObjectTypeAdapter.instance.write(out, (JsonObject) jsonValue);
break;
case STRING:
JsonStringTypeAdapter.instance.write(out, (JsonString) jsonValue);
break;
case NUMBER:
JsonNumberTypeAdapter.instance.write(out, (JsonNumber) jsonValue);
break;
case TRUE:
JsonBooleanTypeAdapter.instance.write(out, jsonValue);
break;
case FALSE:
JsonBooleanTypeAdapter.instance.write(out, jsonValue);
break;
case NULL:
JsonNullTypeAdapter.instance.write(out, jsonValue);
break;
default:
throw new AssertionError(valueType);
}
}
#Override
public JsonValue read(final JsonReader in)
throws IOException {
final JsonToken jsonToken = in.peek();
switch ( jsonToken ) {
case BEGIN_ARRAY:
return JsonArrayTypeAdapter.instance.read(in);
case END_ARRAY:
throw new AssertionError("Must never happen due to delegation to the array type adapter");
case BEGIN_OBJECT:
return JsonObjectTypeAdapter.instance.read(in);
case END_OBJECT:
throw new AssertionError("Must never happen due to delegation to the object type adapter");
case NAME:
throw new AssertionError("Must never happen");
case STRING:
return JsonStringTypeAdapter.instance.read(in);
case NUMBER:
return JsonNumberTypeAdapter.instance.read(in);
case BOOLEAN:
return JsonBooleanTypeAdapter.instance.read(in);
case NULL:
return JsonNullTypeAdapter.instance.read(in);
case END_DOCUMENT:
throw new AssertionError("Must never happen");
default:
throw new AssertionError(jsonToken);
}
}
private static final class JsonNullTypeAdapter
extends TypeAdapter<JsonValue> {
private static final TypeAdapter<JsonValue> instance = new JsonNullTypeAdapter().nullSafe();
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final JsonValue jsonNull)
throws IOException {
out.nullValue();
}
#Override
public JsonValue read(final JsonReader in)
throws IOException {
in.nextNull();
return JsonValue.NULL;
}
}
private static final class JsonBooleanTypeAdapter
extends TypeAdapter<JsonValue> {
private static final TypeAdapter<JsonValue> instance = new JsonBooleanTypeAdapter().nullSafe();
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final JsonValue jsonBoolean)
throws IllegalArgumentException, IOException {
final ValueType valueType = jsonBoolean.getValueType();
switch ( valueType ) {
case TRUE:
out.value(true);
break;
case FALSE:
out.value(false);
break;
case ARRAY:
case OBJECT:
case STRING:
case NUMBER:
case NULL:
throw new IllegalArgumentException("Not a boolean: " + valueType);
default:
throw new AssertionError(jsonBoolean.getValueType());
}
}
#Override
public JsonValue read(final JsonReader in)
throws IOException {
return in.nextBoolean() ? JsonValue.TRUE : JsonValue.FALSE;
}
}
private static final class JsonNumberTypeAdapter
extends TypeAdapter<JsonNumber> {
private static final TypeAdapter<JsonNumber> instance = new JsonNumberTypeAdapter().nullSafe();
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final JsonNumber jsonNumber)
throws IOException {
if ( jsonNumber.isIntegral() ) {
out.value(jsonNumber.longValue());
} else {
out.value(jsonNumber.doubleValue());
}
}
#Override
public JsonNumber read(final JsonReader in)
throws IOException {
// TODO is there a good way to instantiate a JsonNumber instance?
return (JsonNumber) Json.createArrayBuilder()
.add(in.nextDouble())
.build()
.get(0);
}
}
private static final class JsonStringTypeAdapter
extends TypeAdapter<JsonString> {
private static final TypeAdapter<JsonString> instance = new JsonStringTypeAdapter().nullSafe();
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final JsonString jsonString)
throws IOException {
out.value(jsonString.getString());
}
#Override
public JsonString read(final JsonReader in)
throws IOException {
// TODO is there a good way to instantiate a JsonString instance?
return (JsonString) Json.createArrayBuilder()
.add(in.nextString())
.build()
.get(0);
}
}
private static final class JsonObjectTypeAdapter
extends TypeAdapter<JsonObject> {
private static final TypeAdapter<JsonObject> instance = new JsonObjectTypeAdapter().nullSafe();
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final JsonObject jsonObject)
throws IOException {
out.beginObject();
for ( final Entry<String, JsonValue> e : jsonObject.entrySet() ) {
out.name(e.getKey());
jsonValueTypeAdapter.write(out, e.getValue());
}
out.endObject();
}
#Override
public JsonObject read(final JsonReader in)
throws IOException {
final JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
in.beginObject();
while ( in.hasNext() ) {
final String key = in.nextName();
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
jsonObjectBuilder.add(key, jsonValueTypeAdapter.read(in));
break;
case END_ARRAY:
throw new AssertionError("Must never happen due to delegation to the array type adapter");
case BEGIN_OBJECT:
jsonObjectBuilder.add(key, jsonValueTypeAdapter.read(in));
break;
case END_OBJECT:
throw new AssertionError("Must never happen due to delegation to the object type adapter");
case NAME:
throw new AssertionError("Must never happen");
case STRING:
jsonObjectBuilder.add(key, in.nextString());
break;
case NUMBER:
jsonObjectBuilder.add(key, in.nextDouble());
break;
case BOOLEAN:
jsonObjectBuilder.add(key, in.nextBoolean());
break;
case NULL:
in.nextNull();
jsonObjectBuilder.addNull(key);
break;
case END_DOCUMENT:
// do nothing
break;
default:
throw new AssertionError(token);
}
}
in.endObject();
return jsonObjectBuilder.build();
}
}
private static final class JsonArrayTypeAdapter
extends TypeAdapter<JsonArray> {
private static final TypeAdapter<JsonArray> instance = new JsonArrayTypeAdapter().nullSafe();
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final JsonArray jsonArray)
throws IOException {
out.beginArray();
for ( final JsonValue jsonValue : jsonArray ) {
jsonValueTypeAdapter.write(out, jsonValue);
}
out.endArray();
}
#Override
public JsonArray read(final JsonReader in)
throws IOException {
final JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder();
in.beginArray();
while ( in.hasNext() ) {
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
jsonArrayBuilder.add(jsonValueTypeAdapter.read(in));
break;
case END_ARRAY:
throw new AssertionError("Must never happen due to delegation to the array type adapter");
case BEGIN_OBJECT:
jsonArrayBuilder.add(jsonValueTypeAdapter.read(in));
break;
case END_OBJECT:
throw new AssertionError("Must never happen due to delegation to the object type adapter");
case NAME:
throw new AssertionError("Must never happen");
case STRING:
jsonArrayBuilder.add(in.nextString());
break;
case NUMBER:
jsonArrayBuilder.add(in.nextDouble());
break;
case BOOLEAN:
jsonArrayBuilder.add(in.nextBoolean());
break;
case NULL:
in.nextNull();
jsonArrayBuilder.addNull();
break;
case END_DOCUMENT:
// do nothing
break;
default:
throw new AssertionError(token);
}
}
in.endArray();
return jsonArrayBuilder.build();
}
}
}
The code above is itself-document I think, despite it's relatively large. Example use:
private static final Gson gson = new GsonBuilder()
.serializeNulls()
.registerTypeHierarchyAdapter(JsonValue.class, getJsonValueTypeAdapter())
.create();
public static void main(final String... args) {
final JsonValue before = createObjectBuilder()
.add("boolean", true)
.add("integer", 3)
.add("string", "foo")
.addNull("null")
.add("array", createArrayBuilder()
.add(false)
.add(2)
.add("bar")
.addNull()
.build())
.build();
System.out.println("before.toString() = " + before);
final String json = gson.toJson(before);
System.out.println("type adapter result = " + json);
final JsonValue after = gson.fromJson(json, JsonValue.class);
System.out.println("after.toString() = " + after);
}
Output:
before.toString() = {"boolean":true,"integer":3,"string":"foo","null":null,"array":[false,2,"bar",null]}
type adapter result = {"boolean":true,"integer":3,"string":"foo","null":null,"array":[false,2,"bar",null]}
after.toString() = {"boolean":true,"integer":3.0,"string":"foo","null":null,"array":[false,2.0,"bar",null]}
Note that the integer property value has been changed: 3 is now 3.0. This happens because JSON does not distinguish between integers, longs, floats, doubles, etc: all it can handle is just a number. You cannot really restore the original number: for example, 3 may be both long and double. The most you can do here is not using .nextDouble() in favor of .nextString() and trying to detect which numeric type it can fit the most and constuct a JsonNumber instance respectively (I'm wondering how it can be done in javax.json -- see the TODO comments in the type adapter).
Related
I am new to using Gson. I am trying to create a specific typeadapter for my class. Here it is:
public class AssetSerializer extends TypeAdapter<List<Asset>> {
#Override
public void write(JsonWriter out, List<Asset> value) throws IOException {
out.beginArray();
for (Asset asset : value) {
out.beginObject();
out.name("name").value(asset.getName());
out.name("code").value(asset.getCode());
out.endObject();
}
out.endArray();
}
#Override
public List<Asset> read(JsonReader in) throws IOException {
String temp_name = "";
String temp_code = "";
List<Asset> list = new LinkedList<>();
in.beginArray();
while (in.hasNext()) {
switch (in.peek()) {
case BEGIN_OBJECT:
in.beginObject();
while (in.hasNext()) {
switch (in.nextName()) {
case "name":
temp_name = in.nextString();
continue;
case "code":
temp_code = in.nextString();
continue;
}
}
in.endObject();
Asset temp_asset = new Asset(temp_name, temp_code);
list.add(temp_asset);
continue;
}
}
in.endArray();
return list;
}
This is the way I am trying to serialize/deserialize a list of Assets:
Asset asset1 = new Asset("Asset1", "code_1");
Asset asset2 = new Asset("Asset2", "code_2");
LinkedList<Asset> temp_list = new LinkedList<Asset>();
temp_list.add(asset1);
temp_list.add(asset2);
Type assetsType = new TypeToken<List<Asset>>(){}.getType();
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(assetsType, new AssetSerializer())
.create();
String json = gson.toJson(temp_list);
The problem is that my overridden methods are not called in this code, so Gson uses its standard serializer for arrays.
You need to register your custom adapter with GsonBuilder to solve the problem.
I have the following JSON string
[
{
"channel": "/bvmt/initValues",
"data": {
"value": {
"instrumentIds": "['TN0007250012','TN0007500010']",
"instruments": "[{'mnemonic':'ADWYA','marche':'ADWYA','phaut':5.82,'open':5.82,'nbrTrans':7,'veille':5.82,'time':'11:14:28','recapChange':0.00,'state':'','variation':'.','ref':5.82,'stateGrp':'S','percentChange':-0.34,'last':5.80,'bestTransaction':[{'value':5.82,'qte':'3','time':'10:00:00'},{'value':5.82,'qte':'5','time':'10:02:26'},{'value':5.82,'qte':'145','time':'10:23:27'},{'value':5.81,'qte':'100','time':'10:23:42'},{'value':5.80,'qte':'1000','time':'10:23:42'},{'value':5.73,'qte':'1','time':'10:31:21'},{'value':5.80,'qte':'100','time':'11:14:28'}],'volume':7857.19,'id':'TN0007250012','bestLimits':[{'quantiteAchat':2600,'timeVente':'11:44:10','ordreAchat':1,'prixAchat':5.76,'quantiteVente':100,'timeAchat':'11:44:10','ordreVente':1,'prixVente':5.90},{'quantiteAchat':50,'timeVente':'11:44:10','ordreAchat':1,'prixAchat':5.74,'quantiteVente':210,'timeAchat':'11:44:10','ordreVente':1,'prixVente':5.95},{'quantiteAchat':250,'timeVente':'11:44:10','ordreAchat':2,'prixAchat':5.75,'quantiteVente':187,'timeAchat':'11:44:10','ordreVente':1,'prixVente':5.94},{'quantiteAchat':189,'timeVente':'11:44:10','ordreAchat':3,'prixAchat':5.73,'quantiteVente':1112,'timeAchat':'11:44:10','ordreVente':1,'prixVente':5.97},{'quantiteAchat':44,'timeVente':'11:44:10','ordreAchat':1,'prixAchat':5.72,'quantiteVente':400,'timeAchat':'11:44:10','ordreVente':1,'prixVente':5.98}],'openStatus':'','cto':0,'valuer':'ADWYA','pbas':5.73,'grp':'S','abrv':'ADWYA','houv':'','qto':0,'seuilBas':5.65,'vto':0,'quantite':1354,'seuilHaut':5.99},{'mnemonic':'WIFAK','marche':'WIFAK','phaut':7.11,'open':7.00,'nbrTrans':2,'veille':7.13,'time':'10:24:15','recapChange':0.00,'state':'','variation':'.','ref':7.13,'stateGrp':'S','percentChange':-0.28,'last':7.11,'bestTransaction':[{'value':7.00,'qte':'99','time':'10:17:00'},{'value':7.11,'qte':'100','time':'10:24:15'}],'volume':1404.00,'id':'TN0007200017','bestLimits':[{'quantiteAchat':100,'timeVente':'11:00:19','ordreAchat':1,'prixAchat':6.80,'quantiteVente':100,'timeAchat':'11:00:19','ordreVente':1,'prixVente':7.09},{'quantiteAchat':0,'timeVente':'11:00:19','ordreAchat':0,'prixAchat':0.00,'quantiteVente':82,'timeAchat':'11:00:19','ordreVente':1,'prixVente':7.11},{'quantiteAchat':0,'timeVente':'11:00:19','ordreAchat':0,'prixAchat':0.00,'quantiteVente':284,'timeAchat':'11:00:19','ordreVente':2,'prixVente':7.10},{'quantiteAchat':0,'timeVente':'11:00:19','ordreAchat':0,'prixAchat':0.00,'quantiteVente':222,'timeAchat':'11:00:19','ordreVente':1,'prixVente':7.12},{'quantiteAchat':0,'timeVente':'11:00:19','ordreAchat':0,'prixAchat':0.00,'quantiteVente':110,'timeAchat':'11:00:19','ordreVente':2,'prixVente':7.13}],'openStatus':'','cto':0,'valuer':'WIFACK INT BANK','pbas':7.00,'grp':'S','abrv':'WIFAK','houv':'','qto':0,'seuilBas':6.92,'vto':0,'quantite':199,'seuilHaut':7.34}]"
},
"action": "initValues",
"infoSession": {
"lastInstrumentOrder": 11672,
"state": 1,
"lastInstrumentTime": "12:03:00",
"tradingTime": "08:04:02",
"tradingDate": "2017-04-24"
}
},
"id": "5"
},
{
"channel": "/bvmt/process",
"successful": true,
"id": "5"
}
]
I'm interested only in the content of the "instruments" field , I want to get only the "mnemonic" and "percentChange" fields and deserialize them into an array of Objects like this
public class Data
{
public List<MyObject> objects;
}
public class MyObject
{
String mnemonic;
Float percentChange;
}
How can I do this using Gson ?
Actually you have dozen ways of doing it. It only depends on how you manage your JSON documents. Let's declare a couple of DTOs first:
final class Data {
final List<MyObject> objects;
Data(final List<MyObject> objects) {
this.objects = objects;
}
}
final class MyObject {
final String mnemonic;
final Float percentChange;
MyObject(final String mnemonic, final Float percentChange) {
this.mnemonic = mnemonic;
this.percentChange = percentChange;
}
}
Here are some ways:
Pure JsonElement trees
The following example uses Java 8 Stream API and Gson JSON trees facilities, and it appears to be the simplest way to me:
private static final Gson gson = new Gson();
static Data testUsingJsonTreesOnly(final Reader reader) {
final List<MyObject> objects = StreamSupport.stream(gson.fromJson(reader, JsonElement.class).getAsJsonArray().spliterator(), false)
.map(JsonElement::getAsJsonObject)
.map(jsonObject -> jsonObject.get("data"))
.filter(Objects::nonNull)
.map(JsonElement::getAsJsonObject)
.map(jsonObject -> jsonObject.get("value"))
.filter(Objects::nonNull)
.map(JsonElement::getAsJsonObject)
.map(jsonObject -> jsonObject.get("instruments"))
.filter(Objects::nonNull)
.map(JsonElement::getAsJsonPrimitive)
.map(JsonPrimitive::getAsString)
.map(json -> gson.fromJson(json, JsonElement.class))
.map(JsonElement::getAsJsonArray)
.flatMap(jsonArray -> StreamSupport.stream(jsonArray.spliterator(), false))
.map(jsonElement -> gson.fromJson(jsonElement, MyObject.class))
.collect(toList());
return new Data(objects);
}
Two-pass mappings
This approach way attempts to extract the values in two passes:
deserialize the "outer" object;
get the inner object string and deserialize it in the second pass.
private static final Gson gson = new Gson();
private static final Type channelViewListType = new TypeToken<List<ChannelView>>() {
}.getType();
private static final Type myObjectListType = new TypeToken<List<MyObject>>() {
}.getType();
static Data testUsingDeserializationWithStrings(final Reader reader) {
final List<MyObject> objects = gson.<List<ChannelView>>fromJson(reader, channelViewListType)
.stream()
.filter(Objects::nonNull)
.map(view -> view.data)
.filter(Objects::nonNull)
.map(view -> view.value)
.filter(Objects::nonNull)
.map(view -> view.instruments)
.map((Function<String, List<MyObject>>) instruments -> gson.fromJson(instruments, myObjectListType))
.flatMap(Collection::stream)
.collect(toList());
return new Data(objects);
}
private static final class ChannelView {
final DataView data = null;
}
private static final class DataView {
final ValueView value = null;
}
private static final class ValueView {
final String instruments = null;
}
One-pass mappings using type adapters
This is, I would say, level #3: you can implement a specific type adapter to "unwrap" the encoded JSON document. #JsonAdapter can be used to specified the field that contains the specific "inner" JSON document:
private static final Gson gson = new Gson();
private static final Type channelViewListType = new TypeToken<List<ChannelView>>() {
}.getType();
static Data testUsingDeserializationWithJsonAdapter(final Reader reader) {
final List<MyObject> objects = gson.<List<ChannelView>>fromJson(reader, channelViewListType)
.stream()
.filter(Objects::nonNull)
.map(view -> view.data)
.filter(Objects::nonNull)
.map(view -> view.value)
.filter(Objects::nonNull)
.map(view -> view.instruments)
.flatMap(Collection::stream)
.collect(toList());
return new Data(objects);
}
private static final class ChannelView {
final DataView data = null;
}
private static final class DataView {
final ValueView value = null;
}
private static final class ValueView {
#JsonAdapter(UnpackedJsonTypeAdapterFactory.class)
final List<MyObject> instruments = null;
}
private static final class UnpackedJsonTypeAdapterFactory
implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
return new UnpackedJsonTypeAdapter<>(gson.getAdapter(typeToken));
}
private static final class UnpackedJsonTypeAdapter<T>
extends TypeAdapter<T> {
private final TypeAdapter<T> typeAdapter;
private UnpackedJsonTypeAdapter(final TypeAdapter<T> typeAdapter) {
this.typeAdapter = typeAdapter;
}
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final T value)
throws IOException {
out.value(typeAdapter.toJson(value));
}
#Override
public T read(final JsonReader in)
throws IOException {
final String json = in.nextString();
final JsonReader lenientIn = new JsonReader(new StringReader(json));
lenientIn.setLenient(true);
return typeAdapter.read(lenientIn);
}
}
}
Pure streaming
Probably the easiest by concept way, but not that easy to implement because of creating a high-level parser that deals with JSON token stream directly from the beginning to the end. Note that no even Gson instances are introduced.
static Data testUsingStreaming(final Reader reader)
throws IOException {
final List<MyObject> myObjects = new ArrayList<>();
final JsonReader jsonReader = new JsonReader(reader);
jsonReader.beginArray();
while ( jsonReader.hasNext() ) {
jsonReader.beginObject();
while ( jsonReader.hasNext() ) {
switch ( jsonReader.nextName() ) {
case "data":
jsonReader.beginObject();
while ( jsonReader.hasNext() ) {
switch ( jsonReader.nextName() ) {
case "value":
jsonReader.beginObject();
while ( jsonReader.hasNext() ) {
switch ( jsonReader.nextName() ) {
case "instruments":
final String instrumentsJson = jsonReader.nextString();
parseInstruments(instrumentsJson, myObjects);
break;
default:
jsonReader.skipValue();
break;
}
}
jsonReader.endObject();
break;
default:
jsonReader.skipValue();
break;
}
}
jsonReader.endObject();
break;
default:
jsonReader.skipValue();
break;
}
}
jsonReader.endObject();
}
jsonReader.endArray();
return new Data(myObjects);
}
private static void parseInstruments(final String instrumentsJson, final Collection<MyObject> myObjects)
throws IOException {
final JsonReader jsonReader = new JsonReader(new StringReader(instrumentsJson));
jsonReader.setLenient(true);
jsonReader.beginArray();
while ( jsonReader.hasNext() ) {
String mnemonic = null;
Float percentChange = null;
jsonReader.beginObject();
while ( jsonReader.hasNext() ) {
final String name = jsonReader.nextName();
switch ( name ) {
case "mnemonic":
mnemonic = jsonReader.nextString();
break;
case "percentChange":
percentChange = (float) jsonReader.nextDouble();
break;
default:
jsonReader.skipValue();
break;
}
}
if ( mnemonic != null && percentChange != null ) {
myObjects.add(new MyObject(mnemonic, percentChange));
}
jsonReader.endObject();
}
jsonReader.endArray();
}
All of the approaches above produce the same output:
ADWYA: -0.34
WIFAK: -0.28
I would like to cut too long strings in json.
In order to do that I would like to register new type adapter for String type and inside this deserializer I will check and limit too long strings.
Gson gson = new GsonBuilder().registerTypeAdapter(String.class, new CuttingStringDeserializer()).create();
JsonElement element = gson.fromJson(jsonString, JsonElement.class);
return new GsonBuilder().setPrettyPrinting().create().toJson(element);
Example of json file that I want to process:
{
"myString": "this string is too long - cut it",
"other": "this is ok"
}
Desired output:
{
"myString": "this strin",
"other": "this is ok"
}
In general I don't know structure of json but I want to filter all string occurrences.
Deserializer:
public class CuttingStringDeserializer implements JsonDeserializer<String> {
#Override
public String deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
String s = json.getAsString();
if(s.lenght() > MAX_CONTENT_LENGTH){
return s.substring(0, MAX_CONTENT_LENGTH);
}else{
return s;
}
}
Unfortunately my custom deserializer is not called by gson.
This (using some custom JsonWriter) works:
package so41793888;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.stream.JsonWriter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import java.io.IOException;
import java.io.StringWriter;
public class Demo {
public static void main(String[] args) {
String jsonString = "{\n" +
" \"myString\": \"this string is too long - cut it\",\n" +
" \"other\": \"this is ok\"\n" +
"}";
Gson gson = new GsonBuilder().create();
JsonElement element = gson.fromJson(jsonString, JsonElement.class);
StringWriter out = null;
try {
out = new StringWriter();
new GsonBuilder().create().toJson(element, new MyJsonWriter(out));
System.out.println(out.getBuffer().toString());
} finally {
IOUtils.closeQuietly(out);
}
}
private static class MyJsonWriter extends JsonWriter {
public MyJsonWriter(final StringWriter out) {
super(out);
setIndent(" ");
}
#Override
public JsonWriter value(final String value) throws IOException {
return super.value(StringUtils.abbreviate(value, 12));
}
}
}
outputs:
{
"myString": "this stri...",
"other": "this is ok"
}
You can reject the idea of tree processing (the way how JsonSerializer and JsonDeserializer work) in favor of stream processing, where you analyze every token on your own. GsonBuilder seems not to allow overriding a streaming-fashioned TypeAdapters as well, but you can then use JsonReader in order to parse every token from an input stream, and JsonWriter to emit processed tokens to an output stream. This may look too low level, but since it's a streaming way, it is really cheap and does not consume much memory as tree processing usually does. Thus you can process even infinite streams.
#SuppressWarnings("resource")
private static void trim(final int maxStringLength, final Reader reader, final Writer writer)
throws IOException {
// a specifically configured IDEA complains for the unclosed jsonReader, but invoking the `close` method is a like a chain and sometimes undesirable
#SuppressWarnings("all")
final JsonReader jsonReader = new JsonReader(reader);
// the same goes to jsonWriter
#SuppressWarnings("all")
final JsonWriter jsonWriter = new JsonWriter(writer);
for ( JsonToken token; (token = jsonReader.peek()) != END_DOCUMENT; ) {
switch ( token ) {
case BEGIN_ARRAY:
// merely reflect a BEGIN_ARRAY token
jsonReader.beginArray();
jsonWriter.beginArray();
break;
case END_ARRAY:
// merely reflect an END_ARRAY token
jsonReader.endArray();
jsonWriter.endArray();
break;
case BEGIN_OBJECT:
// merely reflect a BEGIN_OBJECT token
jsonReader.beginObject();
jsonWriter.beginObject();
break;
case END_OBJECT:
// merely reflect an END_OBJECT token
jsonReader.endObject();
jsonWriter.endObject();
break;
case NAME:
// merely reflect NAME tokens (or trim?)
jsonWriter.name(jsonReader.nextName());
break;
case STRING:
// trimming a STRING token if necessary
final String string = jsonReader.nextString();
jsonWriter.value(string.length() > maxStringLength ? string.substring(0, maxStringLength) : string);
break;
case NUMBER:
// NUMBER tokens are a bit complex because JSON only denotes a double number that can be literally an integer
final String rawNumber = jsonReader.nextString();
try {
// try to write the biggest integer number supported by Java, floating points also fail to be parsed as long values
jsonWriter.value(parseLong(rawNumber));
} catch ( final NumberFormatException nex1 ) {
try {
// not a long value, then perhaps it's a double value?
jsonWriter.value(parseDouble(rawNumber));
} catch ( final NumberFormatException nex2 ) {
// can't think of specific cases here...
throw new AssertionError("Must not happen", nex2);
}
}
break;
case BOOLEAN:
// merely reflect BOOLEAN tokens
jsonWriter.value(jsonReader.nextBoolean());
break;
case NULL:
// merely reflect NULL tokens
jsonReader.nextNull();
jsonWriter.nullValue();
break;
case END_DOCUMENT:
// fall through, because this type of tokens is checked above, and it's fine to throw an assertion error
default:
throw new AssertionError(token);
}
}
}
This method, of course, does not support pretty printing, but it can be easily implemented if it's really necessary.
And how it's used:
final Reader reader = new StringReader("{\"myString\":\"this string is too long - cut it\",\"other\":\"this is ok\"}");
final Writer writer = new OutputStreamWriter(System.out); // redirect the output to System.out
trim(10, reader, writer);
writer.flush(); // flushing at a call-site, we decide
The output:
{"myString":"this strin","other":"this is ok"}
The solution can work with any kind of JSON, having no background for a particular type. Simply speaking, it's just type-unaware and can process even simple single literals like "foo-bar-baz-qux".
It seems for me it doesn't make sense what are you trying to archive, but here kick off code which should help .
public class Main {
private static String json = "{\"myString\": \"this string is too long - limit it\",\"other\": \"this is ok\"}";
public static void main(String... var) {
System.out.print(cutJson(json));
}
public static String cutJson(String json) {
Type type = new TypeToken<Map<String, String>>() {
}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(type, new CuttingStringDeserializer()).create();
Map<String, String> element = gson.fromJson(json, type);
return new GsonBuilder().setPrettyPrinting().create().toJson(element);
}
private static class CuttingStringDeserializer implements JsonDeserializer<Map<String, String>> {
#Override
public Map<String, String> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Iterator<Map.Entry<String, JsonElement>> iterator = ((JsonObject) json).entrySet().iterator();
Map<String, String> result = new HashMap<String, String>();
while (iterator.hasNext()) {
Map.Entry<String, JsonElement> entry = iterator.next();
if (entry.getValue().getAsString().length() > 10) {
entry.setValue(new JsonPrimitive(entry.getValue().getAsString().substring(0, 9)));
}
result.put(entry.getKey(), entry.getValue().getAsString());
}
return result;
}
}
}
Prints:
{
"myString": "this stri",
"other": "this is ok"
}
I have this Json content:
{
"people":[
{
"name":"test1",
"sirname":"test2",
"details":{
"social_no":1234567,
"creadit_card_no":34582342309
}
},
{
"name":"test3",
"sirname":"test4",
"details":{
"social_no":12345679,
"creadit_card_no":345823423090
}
}
]
}
and according to logic this Json should have 3 POJO classes: A class that will hold the list of People, People object and a Details object.
Now my question is, is it possible to deserialize this Json using Jackson or if not possible with Jackson, with GSON library? One that will contain the list of People, and another one, for example Human class, that will have the following structure:
public class Human{
String name;
String sirname;
String social_no;
String creadit_card_no;
//..getters and setters
//should correspond with this json fragment:
// {
// "name":"test1",
// "sirname":"test2",
// "details":{
// "social_no":1234567,
// "creadit_card_no":34582342309
// }
}
}
So if this is possible, how can I do this?
Update
My actuall json structure is different than the example given here, so here is the original json
So I've created a TypeAdapter on my own, here is the code from this class:
public class PlanTypeAdapter extends TypeAdapter<Plan> {
private final String TAG = PlanTypeAdapter.class.getSimpleName();
#Override
public void write(JsonWriter out, Plan value) throws IOException {
Log.d(TAG, "WRITE");
}
#Override
public Plan read(JsonReader reader) throws IOException {
Log.d(TAG, "READ");
Plan plan = new Plan();
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
reader.setLenient(false);
while (reader.hasNext()) {
Log.d(TAG, "PATH: " + reader.getPath());
Log.d(TAG, "PEEK: " + reader.peek());
if (reader.peek() == JsonToken.BEGIN_OBJECT) {
Log.d(TAG, "BEGIN object, path: " + reader.getPath());
reader.beginObject();
} else if (reader.peek() == JsonToken.NULL) {
Log.d(TAG, "NULL");
reader.skipValue();
} else if (reader.peek() == JsonToken.END_ARRAY) {
Log.d(TAG, "END ARRAY");
if (reader.getPath().contains("retailer")) {
reader.endObject();
} else {
reader.endArray();
}
} else if (reader.peek() == JsonToken.END_OBJECT) {
reader.endObject();
Log.d(TAG, "END object, path: " + reader.getPath());
} else if (reader.peek() == JsonToken.NUMBER) {
Log.d(TAG, "NUMBER " + reader.getPath());
} else if (reader.peek() == JsonToken.BOOLEAN) {
Log.d(TAG, "BOOLEAN " + reader.getPath());
} else if (reader.peek() == JsonToken.NAME) {
switch (reader.nextName()) {
case "retailer":
reader.beginObject();
Log.d(TAG, "RET");
break;
case "national_plan":
reader.beginObject();
Log.d(TAG, "NPlan");
break;
case "name":
if (reader.getPath().contains("retailer")) {
plan.setRetailer_name(reader.nextString());
reader.skipValue();
reader.skipValue();
reader.endObject();
} else {
reader.skipValue();
}
break;
case "contract_end":
plan.setContract_end(reader.nextString());
break;
case "data_level_gb":
plan.setData_level_gb(reader.nextString());
break;
case "data_level_id":
plan.setData_level_id(reader.nextInt());
break;
case "days_to_end":
plan.setDays_to_switch(reader.nextInt());
break;
case "direct_from_operator":
plan.setDirect_from_operator(reader.nextBoolean());
break;
case "calculation_amount":
plan.setCalculationAmount(reader.nextDouble());
break;
case "network_generation_name":
plan.setNetwork_generation_(reader.nextString());
break;
case "partner_plan_id":
plan.setPartner_plan_id(reader.nextString());
break;
case "payment_level":
plan.setPayment_level(reader.nextString());
break;
case "payment_level_id":
plan.setPayment_level_id(reader.nextInt());
break;
case "roaming_amount":
plan.setRoaming_amount(reader.nextDouble());
break;
case "savings_amount":
plan.setSavings_amount(reader.nextDouble());
break;
case "savings_avg":
plan.setSavings_avg(reader.nextDouble());
break;
case "savings_percents":
plan.setSavings_percents(reader.nextInt());
break;
default:
Log.d(TAG, "DEFAULT " + reader.peek() + "");
reader.skipValue();
break;
}
} else {
reader.skipValue();
}
}
return plan;
}
}
If you have a very, very large file, I recommend doing this with a custom deserializer using Gson, but I would not use the JsonDeserializer interface; use the TypeAdapter interface as it is more performant (source). I think that #codemonkey has a very good answer, but it is overly complicated and it can be done much more simply. Specifically, you should never be builidng these Strings yourself (with sb.append()) and you should stay away from JsonDeserializer.
First, create your custom TypeAdapter
public class PersonTypeAdapter extends TypeAdapter<Person> {
#Override
public void write(JsonWriter out, Person value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.beginObject();
out.name("name").value(value.name);
out.name("sirname").value(value.sirname);
out.name("details");
out.beginObject();
out.name("social_no").value(value.social_no);
out.name("creadit_card_no").value(value.creadit_card_no);
out.endObject();
out.endObject();
}
#Override
public Person read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
reader.beginObject();
validateName(reader, "name");
String name = reader.nextString();
validateName(reader, "sirname");
String sirname = reader.nextString();
validateName(reader, "details");
reader.beginObject();
validateName(reader, "social_no");
String social_no = reader.nextString();
validateName(reader, "creadit_card_no");
String creadit_card_no = reader.nextString();
reader.endObject();
reader.endObject();
return new Person(name, sirname, social_no, creadit_card_no);
}
private void validateName(JsonReader reader, String string) throws IOException {
String name = reader.nextName();
if(!string.equals(name)) {
throw new JsonSyntaxException("Expected: \"" + string + "\", got \"" + name + "\"");
}
}
}
And, your POJO, obviously:
public class Person {
public final String name;
public final String sirname;
public final String social_no;
public final String creadit_card_no;
public Person(String name, String sirname, String social_no,
String creadit_card_no) {
this.name = name;
this.sirname = sirname;
this.social_no = social_no;
this.creadit_card_no = creadit_card_no;
}
#Override
public String toString() {
return String.format(
"Person [name=%s, sirname=%s, social_no=%s, creadit_card_no=%s]", name,
sirname, social_no, creadit_card_no);
}
}
Then, you can parse the Json from your file using the method here. /test.json is just the example you gave in your question.
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
public class PersonExample {
public static void main(String... args) {
InputStreamReader streamReader = new InputStreamReader(
PersonExample.class.getResourceAsStream("/test.json"));
PeopleWrapper wrapper = parseJSON(streamReader);
System.out.println(wrapper.people);
}
public static class PeopleWrapper {
#SerializedName("people")
public List<Person> people;
}
public static PeopleWrapper parseJSON(Reader jsonInput) {
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Person.class, new PersonTypeAdapter());
Gson gson = builder.create();
PeopleWrapper peopleWrapper = gson.fromJson(jsonInput, PeopleWrapper.class);
return peopleWrapper;
}
}
This program outputs:
[Person [name=test1, sirname=test2, social_no=1234567, creadit_card_no=34582342309], Person [name=test3, sirname=test4, social_no=12345679, creadit_card_no=345823423090]]
So your actual problem is much more complicated than the one you originally described. I will show you a skeleton of the TypeAdapter you need, and you can figure out the rest. Basically, create the Plan object as you've done, and then for each of the outer JSON keys, handle the value.
If it's one line, you can just handle it in the switch statement.
If it's an array or an object, create a helper method to parse that section of the JSON.
You should assume the JSON is well formed and, if it's not, let Gson throw an Exception. Just tell it to expect what's going to come next.
Here's some code to show you the idea:
import java.io.IOException;
import com.google.gson.*;
import com.google.gson.stream.*;
public class PlanTypeAdapter extends TypeAdapter<Plan> {
private final String TAG = PlanTypeAdapter.class.getSimpleName();
#Override
public void write(JsonWriter out, Plan value) throws IOException {
Log.d(TAG, "WRITE");
}
#Override
public Plan read(JsonReader reader) throws IOException {
Log.d(TAG, "READ");
Plan plan = new Plan();
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
reader.setLenient(false);
reader.beginObject();
while (!(reader.peek() == JsonToken.END_OBJECT)) {
switch (reader.nextName()) {
case "national_plan":
handleNationalPlan(reader, plan);
break;
case "bill_total":
handleBillTotal(reader, plan);
break;
case "contract_end":
plan.setContract_end(reader.nextString());
break;
case "data_level_gb":
plan.setData_level_gb(reader.nextString());
break;
case "data_level_id":
plan.setData_level_id(reader.nextInt());
break;
case "days_to_end":
plan.setDays_to_switch(reader.nextInt());
break;
case "direct_from_operator":
plan.setDirect_from_operator(reader.nextBoolean());
break;
case "calculation_amount":
plan.setCalculationAmount(reader.nextDouble());
break;
case "network_generation_name":
plan.setNetwork_generation_(reader.nextString());
break;
case "partner_plan_id":
plan.setPartner_plan_id(reader.nextString());
break;
case "payment_level":
plan.setPayment_level(reader.nextString());
break;
case "payment_level_id":
plan.setPayment_level_id(reader.nextInt());
break;
case "roaming_amount":
plan.setRoaming_amount(reader.nextDouble());
break;
case "savings_amount":
plan.setSavings_amount(reader.nextDouble());
break;
case "savings_avg":
plan.setSavings_avg(reader.nextDouble());
break;
case "savings_percents":
plan.setSavings_percents(reader.nextInt());
break;
case "yearly_id":
case "handset":
case "internals":
case "consumer_id":
case "calculation_details":
case "operator":
case "total":
case "international_plan":
case "contract_length":
case "zone":
case "externals":
case "cancel_fee":
case "transformers":
case "one-offs":
case "flow":
case "roaming_plan":
case "_id":
// You can use this to ignore the keys you don't care about
default:
Log.d(TAG, "DEFAULT " + reader.peek() + "");
reader.skipValue();
break;
}
}
reader.endObject();
return plan;
}
private void handleNationalPlan(JsonReader reader, Plan plan) throws IOException {
reader.beginObject();
while (!(reader.peek() == JsonToken.END_OBJECT)) {
switch(reader.nextName()) {
case "contract_length":
break;
case "name":
break;
case "country":
// etc.
}
}
reader.endObject();
}
private void handleBillTotal(JsonReader reader, Plan plan) throws IOException {
}
// etc.
}
It seems that at the moment Jackson does not support out of the box such feature to map a field from a nested path.
There is an open issue asking for such feature, but it's a question when will it be done.
The opposite, serializing a nested object to the first level properties in json, is possible by using a #JsonUnwrapped annotation.
So, in order to overcome the problem, it seems that the only way is to write a custom deserializer, which you could map to your class, and use it to create an instance of the class as you need it.
There are different ways you can parse json using the gson library. I will give you two examples.
Method 1 - Write a custom deserializer. This technique uses a class to deserialize the person object. The custom deserializer allows you to create any object you want with the json data. Here are the classes needed to do this:
Group.java:
public class Group {
#SerializedName("people")
private List<Person> persons;
public List<Person> getPersons() {
return persons;
}
public void setPersons(List<Person> persons) {
this.persons = persons;
}
#Override
public String toString() {
String NEW_LINE = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder(this.getClass().getName());
sb.append("{");
sb.append(NEW_LINE);
for(Person p : persons){
sb.append(p.toString());
}
sb.append("}");
return sb.toString();
}
}
GsonTest.java:
public class GsonTest {
public static void main(String[] args) {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Person.class, new PersonDeserializer());
Gson gson = gsonBuilder.create();
try {
JsonParser parser = new JsonParser();
Object obj = parser.parse(new FileReader("C://data.json"));
JsonObject jsonObject = (JsonObject) obj;
Group group = gson.fromJson(jsonObject, Group.class);
System.out.println(group.toString());
} catch (JsonIOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JsonSyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Person.java:
public class Person {
public Person(String name, String sirname, Long social_no, Long creadit_card_no) {
this.name = name;
this.sirname = sirname;
this.social_no = social_no;
this.creadit_card_no = creadit_card_no;
}
private String name;
private String sirname;
private Long social_no;
private Long creadit_card_no;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSirname() {
return sirname;
}
public void setSirname(String sirname) {
this.sirname = sirname;
}
public Long getSocial_no() {
return social_no;
}
public void setSocial_no(Long social_no) {
this.social_no = social_no;
}
public Long getCreadit_card_no() {
return creadit_card_no;
}
public void Long(Long creadit_card_no) {
this.creadit_card_no = creadit_card_no;
}
#Override
public String toString() {
String NEW_LINE = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder(this.getClass().getName());
sb.append("{");
sb.append(NEW_LINE);
sb.append("name: ");
sb.append(name);
sb.append(NEW_LINE);
sb.append("sirname: ");
sb.append(sirname);
sb.append(NEW_LINE);
sb.append("social_no: ");
sb.append(social_no);
sb.append(NEW_LINE);
sb.append("creadit_card_no: ");
sb.append(creadit_card_no);
sb.append(NEW_LINE);
sb.append("}");
sb.append(NEW_LINE);
return sb.toString();
}
}
PersonDeserializer.java
public class PersonDeserializer implements JsonDeserializer<Person> {
public Person deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String name = jsonObject.get("name").getAsString();
String sirname = jsonObject.get("sirname").getAsString();
JsonObject details = jsonObject.get("details").getAsJsonObject();
Long social_no = details.get("social_no").getAsLong();
Long creadit_card_no = details.get("creadit_card_no").getAsLong();
Person person = new Person(name, sirname, social_no, creadit_card_no );
return person;
}
}
Method 2 - Use the JsonReader class to parse the json data. You do not have to load the entire json file at once with this technique. This is a better way to parse a large amount of data on devices that have limited resources. This code will be harder to maintain if the json structure changes though. My example code was inspired by this article http://developer.android.com/reference/android/util/JsonReader.html. Use the Person class above with this new GsonTest class:
public class GsonTest {
List<Person> people = null;
public GsonTest() {
people = new ArrayList<Person>();
}
public static void main(String[] args) {
GsonTest gt = new GsonTest();
gt.doGson();
}
void doGson() {
try {
InputStream is = GsonTest.class.getResourceAsStream("data.json");
JsonReader jsonReader = new JsonReader(new InputStreamReader(is, "UTF-8"));
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("people")) {
readPeopleArray(jsonReader);
}
}
jsonReader.endObject();
for(Person p : people){
System.out.println(p.toString());
}
}
catch (NullPointerException e){
e.printStackTrace();
}
catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void readPeopleArray(JsonReader jsonReader) throws IOException {
jsonReader.beginArray();
while (jsonReader.hasNext()) {
readPersonObject(jsonReader);
}
jsonReader.endArray();
}
private void readPersonObject(JsonReader jsonReader) throws IOException {
String name = null;
String sirname = null;
Long social_no = null;
Long creadit_card_no = null;
jsonReader.beginObject();
while(jsonReader.hasNext()){
String key = jsonReader.nextName();
if(key.equals("details")){
jsonReader.beginObject();
while(jsonReader.hasNext()){
String detailKey = jsonReader.nextName();
if(detailKey.equals("social_no")){
social_no = jsonReader.nextLong();
}
else if(detailKey.equals("creadit_card_no")){
creadit_card_no = jsonReader.nextLong();
}
else{
jsonReader.skipValue();
}
}
jsonReader.endObject();
}
else if(key.equals("name")){
name = jsonReader.nextString();
}
else if(key.equals("sirname")){
sirname = jsonReader.nextString();
}
}
jsonReader.endObject();
people.add(new Person(name, sirname, social_no, creadit_card_no));
}
}
My problem is fairly simple : I have the following simple class:
public class Foo {
private int id = -1;
public void setId(int _id){ this.id = _id; }
public int getId(){ return this.id; }
}
And I am trying to process following JSON:
{
"id": "blah"
}
Obviously, there is a problem here ("blah" cannot be parsed to an int)
Formerly, Jackson throws something like org.codehaus.jackson.map.JsonMappingException: Can not construct instance of java.lang.Integer from String value 'blah': not a valid Integer value
I agree with this, but I'd like to register something somewhere allowing to ignore this type of mapping errors.
I tried with a DeserializationProblemHandler registered (see here) but it seems to only work on unknown properties and not deserialization problems.
Have you any clue on this issue?
I succeeded to solve my problem, thanks to Tatu from Jackson ML.
I had to use custom non blocking deserializers for every primitive types handled in Jackson.
Something like this factory :
public class JacksonNonBlockingObjectMapperFactory {
/**
* Deserializer that won't block if value parsing doesn't match with target type
* #param <T> Handled type
*/
private static class NonBlockingDeserializer<T> extends JsonDeserializer<T> {
private StdDeserializer<T> delegate;
public NonBlockingDeserializer(StdDeserializer<T> _delegate){
this.delegate = _delegate;
}
#Override
public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
return delegate.deserialize(jp, ctxt);
}catch (JsonMappingException e){
// If a JSON Mapping occurs, simply returning null instead of blocking things
return null;
}
}
}
private List<StdDeserializer> jsonDeserializers = new ArrayList<StdDeserializer>();
public ObjectMapper createObjectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule customJacksonModule = new SimpleModule("customJacksonModule", new Version(1, 0, 0, null));
for(StdDeserializer jsonDeserializer : jsonDeserializers){
// Wrapping given deserializers with NonBlockingDeserializer
customJacksonModule.addDeserializer(jsonDeserializer.getValueClass(), new NonBlockingDeserializer(jsonDeserializer));
}
objectMapper.registerModule(customJacksonModule);
return objectMapper;
}
public JacksonNonBlockingObjectMapperFactory setJsonDeserializers(List<StdDeserializer> _jsonDeserializers){
this.jsonDeserializers = _jsonDeserializers;
return this;
}
}
Then calling it like this way (pass as deserializers only those you want to be non blocking) :
JacksonNonBlockingObjectMapperFactory factory = new JacksonNonBlockingObjectMapperFactory();
factory.setJsonDeserializers(Arrays.asList(new StdDeserializer[]{
// StdDeserializer, here, comes from Jackson (org.codehaus.jackson.map.deser.StdDeserializer)
new StdDeserializer.ShortDeserializer(Short.class, null),
new StdDeserializer.IntegerDeserializer(Integer.class, null),
new StdDeserializer.CharacterDeserializer(Character.class, null),
new StdDeserializer.LongDeserializer(Long.class, null),
new StdDeserializer.FloatDeserializer(Float.class, null),
new StdDeserializer.DoubleDeserializer(Double.class, null),
new StdDeserializer.NumberDeserializer(),
new StdDeserializer.BigDecimalDeserializer(),
new StdDeserializer.BigIntegerDeserializer(),
new StdDeserializer.CalendarDeserializer()
}));
ObjectMapper om = factory.createObjectMapper();
You might want to let your controller handle the problem by adding a method that handles this specific exception
#ExceptionHandler(HttpMessageNotReadableException.class)
#ResponseBody
public String handleHttpMessageNotReadableException(HttpMessageNotReadableException ex)
{
JsonMappingException jme = (JsonMappingException) ex.getCause();
return jme.getPath().get(0).getFieldName() + " invalid";
}
Of course, the line
JsonMappingException jme = (JsonMappingException) ex.getCause();
might throw a class cast exception for some cases but i haven't encountered them yet.
I have written a simple error handler which will give you some kind of error which you can return to user with bad request as status code. Use #JsonProperty required = true to get error related to missing properties. Jackson version 2.9.8.
public class JacksonExceptionHandler {
public String getErrorMessage(HttpMessageNotReadableException e) {
String message = null;
boolean handled = false;
Throwable cause = e.getRootCause();
if (cause instanceof UnrecognizedPropertyException) {
UnrecognizedPropertyException exception = (UnrecognizedPropertyException) cause;
message = handleUnrecognizedPropertyException(exception);
handled = true;
}
if (cause instanceof InvalidFormatException) {
InvalidFormatException exception = (InvalidFormatException) cause;
message = handleInvalidFormatException(exception);
handled = true;
}
if (cause instanceof MismatchedInputException) {
if (!handled) {
MismatchedInputException exception = (MismatchedInputException) cause;
message = handleMisMatchInputException(exception);
}
}
if (cause instanceof JsonParseException) {
message = "Malformed json";
}
return message;
}
private String handleInvalidFormatException(InvalidFormatException exception) {
String reference = null;
if (!exception.getPath().isEmpty()) {
String path = extractPropertyReference(exception.getPath());
reference = removeLastCharacter(path);
}
Object value = exception.getValue();
return "Invalid value '" + value + "' for property : " + reference;
}
private String handleUnrecognizedPropertyException(UnrecognizedPropertyException exception) {
String reference = null;
if (!exception.getPath().isEmpty()) {
String path = extractPropertyReference(exception.getPath());
reference = removeLastCharacter(path);
}
return "Unknown property : '" + reference + "'";
}
private String handleMisMatchInputException(MismatchedInputException exception) {
String reference = null;
if (!exception.getPath().isEmpty()) {
reference = extractPropertyReference(exception.getPath());
}
String property = StringUtils.substringBetween(exception.getLocalizedMessage(), "'", "'");
// if property missing inside nested object
if (reference != null && property!=null) {
return "Missing property : '" + reference + property + "'";
}
// if invalid value given to array
if(property==null){
return "Invalid values at : '"+ reference +"'";
}
// if property missing at root level
else return "Missing property : '" + property + "'";
}
// extract nested object name for which property is missing
private String extractPropertyReference(List<JsonMappingException.Reference> path) {
StringBuilder stringBuilder = new StringBuilder();
path.forEach(reference -> {
if(reference.getFieldName() != null) {
stringBuilder.append(reference.getFieldName()).append(".");
// if field is null means it is array
} else stringBuilder.append("[].");
}
);
return stringBuilder.toString();
}
// remove '.' at the end of property path reference
private String removeLastCharacter(String string) {
return string.substring(0, string.length() - 1);
}
}
and call this class object in global advice like this
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String message = new JacksonExceptionHandler().generator.getErrorMessage(ex);
if(message == null){
return ResponseEntity.badRequest().body("Malformed json");
}
return ResponseEntity.badRequest().body(message);
}
Create a simple Mapper:
#Provider
#Produces(MediaType.APPLICATION_JSON)
public class JSONProcessingErroMapper implements ExceptionMapper<InvalidFormatException> {
#Override
public Response toResponse(InvalidFormatException ex) {
return Response.status(400)
.entity(new ClientError("[User friendly message]"))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
DeserializationProblemHandler now has a lot more methods, such as handleUnexpectedToken and handleWeird*Value. It should be able to handle anything one needs.
Subclass it, override methods you need, then add it to your ObjectMapper with addHandler(DeserializationProblemHandler h).