Deserialize a value which can be an array or atomic - java

I'm using Gson and want to deserialize the following JSON alternatives into the same class Values. In the latter case I would like to receive a List of just one element. Is there a way to accomplish this in Gson without having to write a custom deserializer?
//Array:
{
"Value": [205.4, 13.5]
}
//Single value:
{
"Value": 205.4
}
Deserialize both into:
public static class Values {
private List<Double> Value;
}

There is no way to do this in Gson unless using custom TypeAdapter.
public class AwesomeType extends ArrayList<Double> {
...
}
public class AwesomeTypeAdapter extends TypeAdapter<AwesomeType> {
#Override
public void write(JsonWriter out, AwesomeType value) throws IOException {
...
}
#Override
public AwesomeType read(JsonReader in) throws IOException {
AwesomeType result = new AwesomeType();
if (in.peek() == JsonToken.BEGIN_ARRAY) {
in.beginArray();
while (in.peek() != JsonToken.END_ARRAY)
result.add(in.nextDouble());
in.endArray();
} else {
result.add(in.nextDouble());
}
return result;
}
}
Register this type adapter to your Gson and use this class in your model instead of List<Double>.

As #BornToCode mentioned the solution is to write a custom TypeAdapter and register it with the GsonBuilder:
public class ListTypeAdapter extends TypeAdapter<List<Double>> {
#Override
public void write(final JsonWriter writer, final List<Double> doubles) throws IOException {
writer.beginArray();
for (final double value : doubles) {
writer.value(value);
}
writer.endArray();
}
#Override
public List<Double> read(final JsonReader reader) throws IOException {
final List<Double> doubles = new ArrayList<>();
if (reader.peek() == JsonToken.BEGIN_ARRAY) {
reader.beginArray();
while (reader.hasNext()) {
doubles.add(reader.nextDouble());
}
reader.endArray();
} else {
doubles.add(reader.nextDouble());
}
return doubles;
}
}
Register it:
GSONBUILDER.registerTypeAdapter(new TypeToken<List<Double>>(){}.getType(),
new ListTypeAdapter().nullSafe());

Related

How to alter the value of a JsonNode while constructing it from a string in Jackson

I have a JSON string and I want to alter the value while constructing the JsonNode using Jackson library.
eg:-
input: {"name":"xyz","price":"90.00"}
output:{"name":"xyz-3","price":90.90}
I created my own JsonFactory and passed my own Parser. but I can only alter the keys, not the values associated with a key.
code:
private static ObjectMapper create() {
ObjectMapper objectMapper = new ObjectMapper(new JsonFactory() {
#Override
protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(data, offset, len, ctxt));
}
#Override
protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(in, ctxt));
}
#Override
protected JsonParser _createParser(Reader r, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(r, ctxt));
}
#Override
protected JsonParser _createParser(char[] data, int offset, int len, IOContext ctxt, boolean recyclable)
throws IOException {
return new MyParser(super._createParser(data, offset, len, ctxt, recyclable));
}
});
private static final class MyParser extends JsonParserDelegate {
private MyParser(JsonParser d) {
super(d);
}
#Override
public String getCurrentName() throws IOException, JsonParseException {
....
}
#Override
public String getText() throws IOException, JsonParseException {
...
}
#Override
public Object getCurrentValue() {
...
}
#Override
public String getValueAsString() throws IOException {
...
}
#Override
public String getValueAsString(String defaultValue) throws IOException {
...
}
}
Below is the code to construct the JsonNode from the string.
mapper.readTree(jsonStr);
In this case when the readTree method is called the getCurrentValue or getValueAsString methods are not called, so I am not able to alter the value while creating the JsonNode itself.
Also the json strings can be different. Basically I want to construct a JsonNode from the string. so tying to a specific schema/bean is not a good choice here.
How to address this ? TIA
Adding the updated code for version 2.7.4:-
static class MyParser extends JsonParserDelegate {
MyParser(final JsonParser delegate) {
super(delegate);
}
#Override
public String getText() throws IOException {
final String text = super.getText();
if ("name".equals(getCurrentName())) {
return text + "-3";
}
return text;
}
#Override
public JsonToken nextToken() throws IOException {
if ("price".equals(getCurrentName())) {
// Advance token anyway
super.nextToken();
return JsonToken.VALUE_NUMBER_FLOAT;
}
return super.nextToken();
}
#Override
public int getCurrentTokenId() {
try {
if ("price".equals(getCurrentName())) {
return JsonTokenId.ID_NUMBER_FLOAT;
}
} catch (final IOException e) {
//
}
return super.getCurrentTokenId();
}
#Override
public NumberType getNumberType() throws IOException {
if ("price".equals(getCurrentName())) {
return NumberType.FLOAT;
}
return super.getNumberType();
}
#Override
public float getFloatValue() throws IOException {
return Float.parseFloat(getValueAsString("0")) + 0.09F;
}
#Override
public double getDoubleValue() throws IOException {
return Double.parseDouble(getValueAsString("0")) + 0.09D;
}
}
pom.xml:-
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.8.7</version>
<!--<scope>test</scope>-->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.8.7</version>
</dependency>
Edit: there is a subtle difference between 2.7.* and 2.9.*.
While 2.9.* is able to differentiate between double and float with
getDoubleValue()
getFloatValue()
instead 2.7.* only uses
getDoubleValue()
even for ID_NUMBER_FLOAT tokens.
So, you need to decide if you want to maintain retro-compatibility or not.
You can also override both, like I did here.
This is all what you need for your custom MyParser
static class MyParser extends JsonParserDelegate {
MyParser(final JsonParser delegate) {
super(delegate);
}
#Override
public String getText() throws IOException {
final String text = super.getText();
if ("name".equals(getCurrentName())) {
return text + "-3";
}
return text;
}
#Override
public JsonToken nextToken() throws IOException {
if ("price".equals(getCurrentName())) {
// Advance token anyway
super.nextToken();
return JsonToken.VALUE_NUMBER_FLOAT;
}
return super.nextToken();
}
#Override
public int getCurrentTokenId() {
try {
if ("price".equals(getCurrentName())) {
return JsonTokenId.ID_NUMBER_FLOAT;
}
} catch (final IOException e) {
//
}
return super.getCurrentTokenId();
}
#Override
public NumberType getNumberType() throws IOException {
if ("price".equals(getCurrentName())) {
return NumberType.FLOAT;
}
return super.getNumberType();
}
#Override
public float getFloatValue() throws IOException {
return Float.parseFloat(getValueAsString("0")) + 0.09F;
}
#Override
public double getDoubleValue() throws IOException {
return Double.parseDouble(getValueAsString("0")) + 0.09D;
}
}
Output: {"name":"xyz-3","price":90.09}
Your code seems fine, and it's tested and working ;)
Are you really sure that regarding the Separation of Concerns it is a good idea to mix parsing and changes within the parsed data?
If you still want to do this, you could use a Custom Deserializer and treat your wanted field names and types the way you want it, like:
class CustomDeserializer extends StdDeserializer<Entity> {
public CustomDeserializer(Class<Entity> t) {
super(t);
}
#Override
public Entity deserialize(JsonParser jp, DeserializationContext dc) throws IOException {
String name = null;
float price = 0;
JsonToken currentToken = null;
while ((currentToken = jp.nextValue()) != null) {
switch (currentToken) {
case VALUE_STRING:
switch (jp.getCurrentName()) {
case "name":
name = jp.getText() + "-3"; // change this text to whatever you want;
break;
case "price":
price = Float.parseFloat(jp.getText()); // parse
break;
default:
break;
}
break;
default:
break;
}
}
return new Entity(name, price);
}
}
And after registering your custom deserializer it works on any object mapper you want:
#Test
public void customDeserialization() throws IOException {
// given
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Entity.class, new CustomDeserializer(Entity.class));
mapper.registerModule(module);
// when
Entity entity = mapper.readValue("{\"name\":\"xyz\",\"price\":\"90.00\"}", Entity.class);
// then
assertThat(entity.getName()).isEqualTo("xyz-3");
assertThat(entity.getPrice()).isEqualTo(90f);
}

Gson uses TypeAdapter or Json Deserializer to convert data from an error list to an empty list

Let's start from example:
If the data is correct, it should be ( the Beijing cities is empty )
{
"code":200,
"msg":"success",
"data":[
{
"id":1,
"name":"Beijing",
"cities":[]
},
{
"id":2,
"name":"Guangdong",
"cities":[
{
"id":1,
"name":"Guangzhou"
}
]
}
]
}
Now I got a wrong data. ( the Beijing cities is null )
{
"code":200,
"msg":"success",
"data":[
{
"id":1,
"name":"Beijing",
"cities":null
},
{
"id":2,
"name":"Guangdong",
"cities":[
{
"id":1,
"name":"Guangzhou"
}
]
}
]
}
I am using the Retrofit2 ResponseBodyConverter ,the entity class:
public class Result<T> {
private int code;
private String msg;
private T data;
// getters, setters
}
public class Province {
private int id;
private String name;
private List<City> cities;
}
public class City {
private int id;
private String name;
}
The data obtained after deserialization is like this:
but the data I need is like this:
In order to have better fault tolerance, when the data is list, I want to process it by myself.
First of all,I tried to use JsonDeserializer
Gson gson = new GsonBuilder()
.serializeNulls()
.registerTypeHierarchyAdapter(List.class, new GsonListAdapter())
.create();
static class GsonListAdapter implements JsonDeserializer<List<?>> {
#Override
public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonArray()) {
JsonArray array = json.getAsJsonArray();
Type itemType = ((ParameterizedType) typeOfT).getActualTypeArguments()[0];
List list = new ArrayList<>();
for (int i = 0; i < array.size(); i++) {
JsonElement element = array.get(i);
Object item = context.deserialize(element, itemType);
list.add(item);
}
return list;
} else {
return Collections.EMPTY_LIST;
}
}
}
JsonDeserializer is valid when the data is "", {}, and [],but data is null, it will not work.
Then I tried to use TypeAdapter
static class GsonListAdapter extends TypeAdapter<List<?>> {
#Override
public void write(JsonWriter out, List<?> value) throws IOException {
out.value(String.valueOf(value));
}
#Override
public List<?> read(JsonReader reader) throws IOException {
if (reader.peek() != JsonToken.BEGIN_ARRAY) {
reader.skipValue();
return Collections.EMPTY_LIST;
}
return new Gson().fromJson(reader, new TypeToken<List<?>>() {}.getType());
}
}
In this way, no matter what the data is, it can work properly.We know that using TypeToken<List<?>> will give us the LinkedHashMap,So although TypeAdapter can work properly, but I don't know how to convert JsonReader to the List <?>.
So I wonder if there are other ways that I can handle the wrong list data? Or convert JsonReader to the List <?> data I want.
I found the CollectionTypeAdapterFactory in Gson source code.I tried to modify it,it has been tested and it is useful.
public class CollectionTypeAdapterFactory implements TypeAdapterFactory {
private final ConstructorConstructor constructorConstructor;
public CollectionTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
this.constructorConstructor = constructorConstructor;
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Type type = typeToken.getType();
Class<? super T> rawType = typeToken.getRawType();
if (!Collection.class.isAssignableFrom(rawType)) {
return null;
}
Type elementType = $Gson$Types.getCollectionElementType(type, rawType);
TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);
#SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter
TypeAdapter<T> result = new Adapter(gson, elementType, elementTypeAdapter, constructor);
return result;
}
private static final class Adapter<E> extends TypeAdapter<Collection<E>> {
private final TypeAdapter<E> elementTypeAdapter;
private final ObjectConstructor<? extends Collection<E>> constructor;
public Adapter(Gson context, Type elementType,
TypeAdapter<E> elementTypeAdapter,
ObjectConstructor<? extends Collection<E>> constructor) {
this.elementTypeAdapter =
new TypeAdapterRuntimeTypeWrapper<E>(context, elementTypeAdapter, elementType);
this.constructor = constructor;
}
public Collection<E> read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
//In the source code is return null, I changed to return an empty collection
return constructor.construct();
}
Collection<E> collection = constructor.construct();
in.beginArray();
while (in.hasNext()) {
E instance = elementTypeAdapter.read(in);
collection.add(instance);
}
in.endArray();
return collection;
}
public void write(JsonWriter out, Collection<E> collection) throws IOException {
if (collection == null) {
out.nullValue();
return;
}
out.beginArray();
for (E element : collection) {
elementTypeAdapter.write(out, element);
}
out.endArray();
}
}
}
In the source code the TypeAdapterRuntimeTypeWrapper is protected,We must make a copy.
public class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
private final Gson context;
private final TypeAdapter<T> delegate;
private final Type type;
TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> delegate, Type type) {
this.context = context;
this.delegate = delegate;
this.type = type;
}
#Override
public T read(JsonReader in) throws IOException {
return delegate.read(in);
}
#SuppressWarnings({"rawtypes", "unchecked"})
#Override
public void write(JsonWriter out, T value) throws IOException {
TypeAdapter chosen = delegate;
Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value);
if (runtimeType != type) {
TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType));
if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {
// The user registered a type adapter for the runtime type, so we will use that
chosen = runtimeTypeAdapter;
} else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
// The user registered a type adapter for Base class, so we prefer it over the
// reflective type adapter for the runtime type
chosen = delegate;
} else {
// Use the type adapter for runtime type
chosen = runtimeTypeAdapter;
}
}
chosen.write(out, value);
}
private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
if (value != null
&& (type == Object.class || type instanceof TypeVariable<?> || type instanceof Class<?>)) {
type = value.getClass();
}
return type;
}
}
How to use
Gson gson = new GsonBuilder().serializeNulls()
.registerTypeAdapterFactory(
new CollectionTypeAdapterFactory(new ConstructorConstructor(new HashMap<>()))
)
.create();
Result<List<Province>> result = gson.fromJson(jsonStr, new TypeToken<Result<List<Province>>>() {}.getType());
prints:
Result{code=200, msg='success', data=[Province{id=1, name='Beijing', cities=[]}, Province{id=2, name='Guangdong', cities=[City{id=1, name='Guangzhou'}]}]}

Deserializing JSON object to string field with GSON

I have a POJO with a String field that is already serialized JSON. Performance is key here, so I want to avoid parsing it and then re-serializing it.
public class SomeObject {
String someString = "";
String jsonString = "{\"one\":4, \"two\":\"hello\"}";
long someLong = 4;
}
Currently GSON serialises it like so:
{ "someString":"", "jsonString":"{\"one\":4, \"two\":\"hello\"}", "someLong":4 }
I wrote a JsonSerializer/Deserializer in hopes of using the #JsonAdapter annotation, but it only supports TypeAdapter or TypeAdapterFactory.
public class JsonStringTypeAdapter implements JsonSerializer<String>, JsonDeserializer<String> {
#Override
public JsonElement serialize(String t, Type type, JsonSerializationContext jsc) {
return new JsonParser().parse(t).getAsJsonObject();
}
#Override
public String deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
return je.getAsString();
}
#Override
public void write(JsonWriter writer, String t) throws IOException {
writer.jsonValue(t);
}
}
So I wrote the following simple TypeAdapter that works perfectly for serialisation, but I can't work out how to deserialise a Json object to String in a TypeAdapter.
public class JsonStringTypeAdapter extends TypeAdapter<String> {
#Override
public void write(JsonWriter writer, String t) throws IOException {
writer.jsonValue(t);
}
#Override
public String read(JsonReader reader) throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
}
I know Jackson has an annotation for this. Any ideas for doing it with GSON?
Solved it using TypeAdapterFactory.
public class JsonStringTypeAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> tokenType) {
if (!JsonString.class.isAssignableFrom(tokenType.getRawType())) return null;
return (TypeAdapter<T>) new JsonStringTypeAdapter(gson);
}
}
And completing the read method
/* The JsonStringTypeAdapter writes the raw string value directly to the JSON output
* this offers great performance by avoiding parsing then reserialising
* Note: Care must be taken to ensure the input JsonString is well formed JSON.
* Otherwise, when it is deserialised, errors will occur.
*
* #author adamjohnson
*/
public class JsonStringTypeAdapter extends TypeAdapter<JsonString> {
private final Gson gson;
public JsonStringTypeAdapter(Gson gson) {
this.gson = gson;
}
#Override
public void write(JsonWriter writer, JsonString t) throws IOException {
/* check for invalid json string, if so create empty object. */
if (t.value().equals("")) {
writer.jsonValue("{}");
} else /* write raw string directly to json output */ {
writer.jsonValue(t.value());
}
}
#Override
public JsonString read(JsonReader reader) throws IOException {
return new JsonString(gson.getAdapter(JsonElement.class).read(reader).getAsString());
}
}

GSON: serialize/deserialize object of class, that have registered type hierarchy adapter, using ReflectiveTypeAdapterFactory.Adapter

To be clear, let introduse some model:
interface A {
boolean isSomeCase();
}
class AAdapter implements JsonSerializer<A> {
public JsonElement serialize(A src, Type typeOfSrc, JsonSerializationContext context) {
if (src.isSomeCase()) {
/* some logic */
return result;
} else {
JsonObject json = new JsonObject();
JsonElement valueJson = <???>; // TODO serialize src like POJO
json.add(src.getClass().getSimpleName(), valueJson);
return json;
}
}
}
Gson gson = new GsonBuilder()
.registerTypeHierarchyAdapter(A.class. new AAdapter())
.create();
How it is possible to serealize some instance of A, which isSomeCase() = false, like any other object, that is serialized by ReflectiveTypeAdapterFactory.Adapter.
You can write a custom TypeAdapterFactory and handle incoming object's isSomeCase() result in its TypeAdapter's write() method and apply your logic there:
public class ATypeAdapterFactory implements TypeAdapterFactory {
public TypeAdapter<A> create(Gson gson, TypeToken type) {
if (!A.class.isAssignableFrom(type.getRawType())) {
// Check if incoming raw type is an instance of A interface
return null;
}
final TypeAdapter<A> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<A>() {
#Override
public void write(JsonWriter out, A value) throws IOException {
if(value.isSomeCase()) {
// your custom logic here
out.beginObject();
out.name("x").value(0);
out.endObject();
} else {
// default serialization here
delegate.write(out, value);
}
}
#Override
public A read(JsonReader in) throws IOException {
return delegate.read(in);
}
};
}
}
Test:
final GsonBuilder gsonBuilder = new GsonBuilder();
// Register custom type adapter factory
gsonBuilder.registerTypeAdapterFactory(new ATypeAdapterFactory());
final Gson gson = gsonBuilder.create();
A aSomeCaseTrue = new AImpl(true);
System.out.print("aSomeCaseTrue:" + gson.toJson(aSomeCaseTrue));
// writes; aSomeCaseTrue:{"x":0}
A aSomeCaseFalse = new AImpl(false);
System.out.print("aSomeCaseFalse:" + gson.toJson(aSomeCaseFalse););
// writes; aSomeCaseFalse:{"someCase":false}
Extras:
1) Your interface:
interface A {
boolean isSomeCase();
}
2) A sample class which implements your sample interface:
class AImpl implements A {
boolean someCase;
public AImpl(boolean value) {
this.someCase = value;
}
#Override
public boolean isSomeCase() {
return someCase;
}
}

Gson: How to change output of Enum

I've this enum:
enum RequestStatus {
OK(200), NOT_FOUND(400);
private final int code;
RequestStatus(int code) {
this.code = code;
}
public int getCode() {
return this.code;
}
};
and in my Request-class, I have this field: private RequestStatus status.
When using Gson to convert the Java object to JSON the result is like:
"status": "OK"
How can I change my GsonBuilder or my Enum object to give me an output like:
"status": {
"value" : "OK",
"code" : 200
}
You can use something like this:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapterFactory(new MyEnumAdapterFactory());
or more simply (as Jesse Wilson indicated):
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(RequestStatus.class, new MyEnumTypeAdapter());
and
public class MyEnumAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
Class<? super T> rawType = type.getRawType();
if (rawType == RequestStatus.class) {
return new MyEnumTypeAdapter<T>();
}
return null;
}
public class MyEnumTypeAdapter<T> extends TypeAdapter<T> {
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
RequestStatus status = (RequestStatus) value;
// Here write what you want to the JsonWriter.
out.beginObject();
out.name("value");
out.value(status.name());
out.name("code");
out.value(status.getCode());
out.endObject();
}
public T read(JsonReader in) throws IOException {
// Properly deserialize the input (if you use deserialization)
return null;
}
}
}
In addition to Polet's answer, if you need a generic Enum serializer, you can achieve it via reflection:
public class EnumAdapterFactory implements TypeAdapterFactory
{
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type)
{
Class<? super T> rawType = type.getRawType();
if (rawType.isEnum())
{
return new EnumTypeAdapter<T>();
}
return null;
}
public class EnumTypeAdapter<T> extends TypeAdapter<T>
{
#Override
public void write(JsonWriter out, T value) throws IOException
{
if (value == null || !value.getClass().isEnum())
{
out.nullValue();
return;
}
try
{
out.beginObject();
out.name("value");
out.value(value.toString());
Arrays.stream(Introspector.getBeanInfo(value.getClass()).getPropertyDescriptors())
.filter(pd -> pd.getReadMethod() != null && !"class".equals(pd.getName()) && !"declaringClass".equals(pd.getName()))
.forEach(pd -> {
try
{
out.name(pd.getName());
out.value(String.valueOf(pd.getReadMethod().invoke(value)));
} catch (IllegalAccessException | InvocationTargetException | IOException e)
{
e.printStackTrace();
}
});
out.endObject();
} catch (IntrospectionException e)
{
e.printStackTrace();
}
}
public T read(JsonReader in) throws IOException
{
// Properly deserialize the input (if you use deserialization)
return null;
}
}
}
Usage:
#Test
public void testEnumGsonSerialization()
{
List<ReportTypes> testEnums = Arrays.asList(YourEnum.VALUE1, YourEnum.VALUE2);
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapterFactory(new EnumAdapterFactory());
Gson gson = builder.create();
System.out.println(gson.toJson(reportTypes));
}

Categories

Resources