How can I get JSON to skip serialization of fields with specific default values? I can for example annotate the fields with a custom annotation for a TypeAdapter to parse, however I struggle finding out how to write such TypeAdapter without completely reinventing the wheel (i.e. I could skip the write method of ReflectiveTypeAdapterFactory and write my own with reflection).
Background: I'm sending a GUI over Json, and I want to expand e.g. a panel widget with every possible property (background, border, etc.) but not send all of those as null values because most panels use default values anyway.
POJO:
public class LabelWidget {
private final String label;
#DefaultValue("c") private final String align;
#DefaultValue("12") private final String size;
public LabelWidget(String label, String align, String size) {
...
}
public String getLabel() {return label;}
public String getAlign() {return align==null||align.isEmpty() ? "c" : align;}
public String getSize() {return size==null||size.isEmpty() ? "12" : size;}
}
Objects:
a = new LabelWidget("foo", "c", "12");
b = new LabelWidget("bar", "l", "50%");
Wanted results:
{label:"foo"}
{label:"bar", align:"l", size:"50%"}
Not sure how #DefaultValue integrates with GSON but one of the solution that works is to actually nullify fields in case of default values during construction time e.g.:
public LabelWidget(String label, String align, String size) {
this.label = label;
this.align = StringUtils.isBlank(align) || "c".equals(align) ? null : align;
this.size = StringUtils.isBlank(size) || "12".equals(size) ? null : size;
}
In such case your getters will return correct values and GSON will not serialize null values.
There are no options in Gson like that to accomplish your request and you still have to process such an annotation yourself. Ideally, it would be great if Gson could provide visiting ReflectiveTypeAdapterFactory, or probably enhance ExclusionStrategy in order to access fields values along with associated fields. However, none of those are available, but it's possible to take one of the following options:
convert your value objects to Map<String, Object> instances (requires intermediate objects to be constructed; probably expensive);
re-implement a #DefaultValue-aware ReflectiveTypeAdapterFactory (I guess it's the best solution, but probably it could be even more generalized);
temporarily strip the #DefaultValue-annotated fields from serialized objects and revert their state back once they are serialized (potentially unsafe and probably a performance hit);
clone values and stripping the values according to nulls so no worrying about reverting back (may be expensive too).
Option #3 can be implemented as follows:
#Target(FIELD)
#Retention(RUNTIME)
#interface DefaultValue {
String value() default "";
}
final class DefaultValueTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory defaultValueTypeAdapterFactory = new DefaultValueTypeAdapterFactory();
private DefaultValueTypeAdapterFactory() {
}
static TypeAdapterFactory getDefaultValueTypeAdapterFactory() {
return defaultValueTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( DefaultValueTypeAdapter.hasDefaults(typeToken.getType()) ) {
return new DefaultValueTypeAdapter<>(gson.getDelegateAdapter(this, typeToken));
}
return null;
}
private static final class DefaultValueTypeAdapter<T>
extends TypeAdapter<T> {
private final TypeAdapter<T> delegateAdapter;
private DefaultValueTypeAdapter(final TypeAdapter<T> delegateAdapter) {
this.delegateAdapter = delegateAdapter;
}
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
final Map<Field, Object> defaults = getDefaults(value);
try {
resetFields(value, defaults.keySet());
delegateAdapter.write(out, value);
} finally {
setFields(value, defaults);
}
}
#Override
public T read(final JsonReader in)
throws IOException {
final T value = delegateAdapter.read(in);
trySetAnnotationDefaults(value);
return value;
}
private static boolean hasDefaults(final Type type) {
if ( !(type instanceof Class) ) {
return false;
}
final Class<?> c = (Class<?>) type;
return Stream.of(c.getDeclaredFields())
.flatMap(f -> Stream.of(f.getAnnotationsByType(DefaultValue.class)))
.findAny()
.isPresent();
}
private static Map<Field, Object> getDefaults(final Object o) {
if ( o == null ) {
return emptyMap();
}
final Class<?> c = o.getClass();
final Map<Field, Object> map = Stream.of(c.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(DefaultValue.class))
.filter(f -> !f.getType().isPrimitive()) // primitive fields cause ambiguities
.peek(f -> f.setAccessible(true))
.filter(f -> {
final String defaultValue = f.getAnnotation(DefaultValue.class).value();
final String comparedValue = ofNullable(getUnchecked(o, f)).map(Object::toString).orElse(null);
return defaultValue.equals(comparedValue);
})
.collect(toMap(identity(), f -> getUnchecked(o, f)));
return unmodifiableMap(map);
}
private static void trySetAnnotationDefaults(final Object o) {
if ( o == null ) {
return;
}
final Class<?> c = o.getClass();
Stream.of(c.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(DefaultValue.class))
.forEach(f -> {
f.setAccessible(true);
if ( getUnchecked(o, f) == null ) {
final String annotationValue = f.getAnnotation(DefaultValue.class).value();
setOrDefaultUnchecked(o, f, parseDefaultValue(f.getType(), annotationValue));
}
});
}
private static Object parseDefaultValue(final Class<?> type, final String rawValue) {
if ( type == String.class ) {
return rawValue;
}
if ( type == Boolean.class ) {
return Boolean.valueOf(rawValue);
}
if ( type == Byte.class ) {
return Byte.valueOf(rawValue);
}
if ( type == Short.class ) {
return Short.valueOf(rawValue);
}
if ( type == Integer.class ) {
return Integer.valueOf(rawValue);
}
if ( type == Long.class ) {
return Long.valueOf(rawValue);
}
if ( type == Float.class ) {
return Float.valueOf(rawValue);
}
if ( type == Double.class ) {
return Double.valueOf(rawValue);
}
if ( type == Character.class ) {
final int length = rawValue.length();
if ( length != 1 ) {
throw new IllegalArgumentException("Illegal raw value length: " + length + " for " + rawValue);
}
return rawValue.charAt(0);
}
throw new AssertionError(type);
}
private static void resetFields(final Object o, final Iterable<Field> fields) {
fields.forEach(f -> setOrDefaultUnchecked(o, f, null));
}
private static void setFields(final Object o, final Map<Field, Object> defaults) {
if ( o == null ) {
return;
}
defaults.entrySet().forEach(e -> setOrDefaultUnchecked(o, e.getKey(), e.getValue()));
}
private static Object getUnchecked(final Object o, final Field field) {
try {
return field.get(o);
} catch ( final IllegalAccessException ex ) {
throw new RuntimeException(ex);
}
}
private static void setOrDefaultUnchecked(final Object o, final Field field, final Object value) {
try {
field.set(o, value);
} catch ( final IllegalAccessException ex ) {
throw new RuntimeException(ex);
}
}
}
}
So:
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getDefaultValueTypeAdapterFactory())
.create();
final LabelWidget before = new LabelWidget("label", "c", "12");
out.println(before);
final String json = gson.toJson(before);
out.println(json);
final LabelWidget after = gson.fromJson(json, LabelWidget.class);
out.println(after);
LabelWidget{label='label', align='c', size='12'}
{"label":"label"}
LabelWidget{label='label', align='c', size='12'}
Or you might also re-consider the design of your data transfer architecture, and probably proceed with just nulls (that however does not let distinguish between "really" null and something like undefined).
Related
I have a message in JSON format that I converted to a JSONObject, and I have around 30 mandatory fields that I have to check for whether they're null or not. If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message. Is there any efficient way I can do this without going through each and every field and using isNull() ?
Also, the JSON objects are nested, so a simple anyNull() function would not work since it would only return if the object itself is null and not if the variables themselves are null.
I tried using gson to convert the message to a POJO, and created classes for 10 objects
Gson gson = new Gson();
Message message = gson.fromJson(msg, Message.class);
but since many classes are nested (and one of which is an array of objects) using simple null checkers don't work.
Actually speaking your question is not very clear because you're using a word of "message" that refers your particular class, but can also be more generic referring sent/received messages.
So something like for JSON elements in memory:
public static void failOnNullRecursively(final JsonElement jsonElement) {
if ( jsonElement.isJsonNull() ) {
throw new IllegalArgumentException("null!");
}
if ( jsonElement.isJsonPrimitive() ) {
return;
}
if ( jsonElement.isJsonArray() ) {
for ( final JsonElement element : jsonElement.getAsJsonArray() ) {
failOnNullRecursively(element);
}
return;
}
if ( jsonElement.isJsonObject() ) {
for ( final Map.Entry<String, JsonElement> e : jsonElement.getAsJsonObject().entrySet() ) {
failOnNullRecursively(e.getValue());
}
return;
}
throw new AssertionError(jsonElement);
}
or JSON documents in streams:
public final class FailOnNullJsonReader
extends JsonReader {
private FailOnNullJsonReader(final Reader reader) {
super(reader);
}
public static JsonReader create(final Reader reader) {
return new FailOnNullJsonReader(reader);
}
#Override
public void nextNull() {
throw new IllegalStateException(String.format("null at %#!", getPath()));
}
}
Both of them will throw on null. But it also seems that you want to validate Message instances:
If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message.
So this tells why the above null-checks won't fit your needs. What you're looking for is JSR-303. It won't be that efficient as you might want to want it to be (message instances are deserialized, validation takes time and resources too), but it might be efficient from the coding perspective:
final Set<ConstraintViolation<V>> violations = validator.validate(message);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
or even integrate it right into Gson so that it serves middleware:
public final class PostReadTypeAdapterFactory<V>
implements TypeAdapterFactory {
private final Predicate<? super TypeToken<?>> supports;
private final BiConsumer<? super TypeToken<V>, ? super V> onRead;
private PostReadTypeAdapterFactory(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
this.supports = supports;
this.onRead = onRead;
}
public static <V> TypeAdapterFactory create(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
return new PostReadTypeAdapterFactory<>(supports, onRead);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !supports.test(typeToken) ) {
return null;
}
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
final T readValue = delegate.read(in);
#SuppressWarnings("unchecked")
final V value = (V) readValue;
#SuppressWarnings("unchecked")
final TypeToken<V> valueTypeToken = (TypeToken<V>) typeToken;
onRead.accept(valueTypeToken, value);
return readValue;
}
};
}
}
public final class Jsr303Support {
private Jsr303Support() {
}
public static <V> TypeAdapterFactory createTypeAdapterFactory(final Validator validator) {
return PostReadTypeAdapterFactory.<V>create(
typeToken -> typeToken.getRawType().isAnnotationPresent(Validate.class),
(typeToken, value) -> {
final Set<ConstraintViolation<V>> violations = validator.validate(value);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
}
);
}
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface Validate {
}
And the test (using Lombok for brevity):
#Validate
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#NotNull
final String foo;
#NotNull
final String bar;
#NotNull
final String baz;
}
public final class Jsr303SupportTest {
private static final Validator validator;
static {
try ( final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory() ) {
validator = validatorFactory.getValidator();
}
}
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(Jsr303Support.createTypeAdapterFactory(validator))
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final ConstraintViolationException ex = Assertions.assertThrows(ConstraintViolationException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals(1, ex.getConstraintViolations().size());
}
}
And finally, probably the most efficient (in terms of reading JSON stream), but very limited whencompared to JSR-303 (and NOT working in Gson because Gson does not propagate null-checking to downstream (de)serializers), way that could replace #NotNull with a similar "functional" annotation:
public final class NotNullTypeAdapterFactory
implements TypeAdapterFactory {
// note no external access
private NotNullTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final TypeAdapter<T> delegate = gson.getAdapter(typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, #Nullable final T value)
throws IOException {
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null");
}
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
#Nullable
final T value = delegate.read(in);
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null at " + in.getPath());
}
return value;
}
};
}
}
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String foo;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String bar;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String baz;
}
public final class NotNullTypeAdapterFactoryTest {
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals("whatever here, the above does not work anyway", ex.getMessage());
}
}
The third, JSR-303, looks like the best for you.
I've been using Gson for a few weeks and I've discovered the Runtime Type Adapter Factory class that allows to "adapt values whose runtime type may differ from their declaration type".
Here's my current code using Gson:
public class Database {
private final Gson gson;
private Database() {
// Initialize Gson
RuntimeTypeAdapterFactory<Base> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Base.class, "table")
.registerSubtype(AdminsTbl.class, "admins");
this.gson = new GsonBuilder().registerTypeAdapterFactory(runtimeTypeAdapterFactory).create();
}
}
I have a "table" field in my JSON that tells Gson which class to use (in this case: "admins" -> AdminsTbl.class). Is there something like this in Moshi?
In fact, Gson does not provide RuntimeTypeAdapterFactory per se in its standard bundle. Quick googling for a Moshi implementation brings your question at the top of the search results, at least for me. :) I also couldn't find anything like that in the com.squareup.moshi.recipes package. But you can easily implement it yourself:
public final class MoshiRuntimeTypeJsonAdapterFactory
implements JsonAdapter.Factory {
private static final String DEFAULT_CLASS_NAME_PROPERTY = "type";
private final Class<?> baseClass;
private final String classNameProperty;
private final Map<String, Class<?>> classNameToClass = new HashMap<>();
private final Map<Class<?>, String> classToClassName = new HashMap<>();
private MoshiRuntimeTypeJsonAdapterFactory(final Class<?> baseClass, final String classNameProperty) {
this.baseClass = baseClass;
this.classNameProperty = classNameProperty;
}
public static MoshiRuntimeTypeJsonAdapterFactory of(final Class<?> expectedClass) {
return new MoshiRuntimeTypeJsonAdapterFactory(expectedClass, DEFAULT_CLASS_NAME_PROPERTY);
}
public static MoshiRuntimeTypeJsonAdapterFactory of(final Class<?> expectedClass, final String classNameProperty) {
return new MoshiRuntimeTypeJsonAdapterFactory(expectedClass, classNameProperty);
}
public MoshiRuntimeTypeJsonAdapterFactory with(final Class<?> concreteClass) {
return with(concreteClass, concreteClass.getSimpleName());
}
public MoshiRuntimeTypeJsonAdapterFactory with(final Class<?> concreteClass, final String className)
throws IllegalArgumentException {
if ( classNameToClass.containsKey(className) ) {
throw new IllegalArgumentException(className + " is already registered for " + concreteClass);
}
if ( classToClassName.containsKey(concreteClass) ) {
throw new IllegalArgumentException(concreteClass + " is already registered for " + className);
}
classNameToClass.put(className, concreteClass);
classToClassName.put(concreteClass, className);
return this;
}
#Nullable
#Override
public JsonAdapter<?> create(final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
if ( !(type instanceof Class) ) {
return null;
}
final Class<?> typeAsClass = (Class<?>) type;
if ( !baseClass.isAssignableFrom(typeAsClass) ) {
return null;
}
final JsonAdapter<Object> jsonObjectJsonAdapter = moshi.nextAdapter(this, Map.class, ImmutableSet.of());
final LoadingCache<Class<?>, JsonAdapter<Object>> jsonAdaptersCache = CacheBuilder.newBuilder()
.build(new CacheLoader<Class<?>, JsonAdapter<Object>>() {
#Override
public JsonAdapter<Object> load(final Class<?> clazz) {
return moshi.nextAdapter(MoshiRuntimeTypeJsonAdapterFactory.this, clazz, ImmutableSet.copyOf(clazz.getAnnotations()));
}
});
return new JsonAdapter<Object>() {
#Nullable
#Override
public Object fromJson(final JsonReader jsonReader)
throws IOException {
try {
#SuppressWarnings("unchecked")
final Map<String, Object> jsonObject = (Map<String, Object>) jsonReader.readJsonValue();
assert jsonObject != null;
final Object rawClassName = jsonObject.get(classNameProperty);
if ( !(rawClassName instanceof String) ) {
throw new IOException("Type name: expected a string in " + classNameProperty + ", but got " + rawClassName);
}
final String className = (String) rawClassName;
final Class<?> concreteClass = classNameToClass.get(className);
if ( concreteClass == null ) {
throw new IOException("No mapping registered for " + className);
}
final JsonAdapter<Object> jsonAdapter = jsonAdaptersCache.get(concreteClass);
return jsonAdapter.fromJsonValue(jsonObject);
} catch ( final ExecutionException ex ) {
throw new RuntimeException(ex);
}
}
#Override
public void toJson(final JsonWriter jsonWriter, #Nullable final Object value)
throws IOException {
try {
assert value != null;
final Class<?> concreteClass = value.getClass();
final String className = classToClassName.get(concreteClass);
if ( className == null ) {
throw new IOException("No mapping registered for " + concreteClass);
}
final JsonAdapter<Object> valueJsonAdapter = jsonAdaptersCache.get(concreteClass);
#SuppressWarnings("unchecked")
final Map<String, Object> jsonObject = (Map<String, Object>) valueJsonAdapter.toJsonValue(value);
assert jsonObject != null;
jsonObject.put(classNameProperty, className);
jsonObjectJsonAdapter.toJson(jsonWriter, jsonObject);
} catch ( final ExecutionException ex ) {
throw new RuntimeException(ex);
}
}
};
}
}
This implementation makes some use of Google Guava for immutable collections (ImmutableSet) and caching (LoadingCache), but you can easily replace them on your own. I also believe this implementation can be improved for potential Moshi-related performance issues as well.
The trivial example from RuntimeTypeAdapterFactory as seen here adaptation:
private static final Moshi moshi = new Moshi.Builder()
.add(MoshiRuntimeTypeJsonAdapterFactory.of(Shape.class)
.with(Shape.Circle.class)
.with(Shape.Diamond.class)
.with(Shape.Rectangle.class)
)
.build();
I have a huge JSON file titled something.json. The file is 20 MB. I'm reading this in with GSON. It's being read on a standard Android Nexus 5X.
Example of the Json:
[
{"country":"UA","name":"Hurzuf","_id":707860,"coord":{"lon":34.283333,"lat":44.549999}},
{"country":"UA","name":"Il’ichëvka","_id":707716,"coord":{"lon":34.383331,"lat":44.666668}},
{"country":"BG","name":"Rastnik","_id":727762,"coord":{"lon":25.283331,"lat":41.400002}}
...
]
Code:
#Override
protected ArrayList<City> doInBackground(File... files) {
ArrayList<City> cities = new ArrayList<>();
try {
InputStream is = new FileInputStream(files[0]);
JsonReader reader = new JsonReader(new InputStreamReader(is, "UTF-8"));
reader.beginArray();
while (reader.hasNext()) {
City city = new Gson().fromJson(reader, City.class);
cities.add(city);
}
reader.endArray();
reader.close();
} catch (Exception e) {
mResult.onFinish(cities, e.getMessage());
}
Collections.sort(cities, (o1, o2) -> o1.getName().compareTo(o2.getName()));
mResult.onFinish(cities, CityService.SUCCESS);
return cities;
}
Library used:
com.google.code.gson:gson:2.8.0
It needs to work from Android API 16 till the latest.
I need to read this in to mCities, and sort it alphabetically on city name. Right now this takes 3 minutes and it has to be done in around 10 seconds. My approach is to cut up the json file in 10 smaller chunks, read these in, concatenate and sort them.
So my question is: how to divide the file in smaller chunks and is this the correct approach to solve this problem?
Link to the file: http://www.jimclermonts.nl/docs/cities.json
I mostly never do Android coding per se, but I have some notes and probably ideas for you to go with since this is pure Java.
Your reader does very excessive work while reading each element.
First of all, you don't need to create Gson every time you need it:
It's immutable and thread-safe.
It's relatively expensive to create.
Instantiating a Gson instance also hits the heap taking more time to execute and then garbage-collect.
Next, there is a difference between only-deserialization and JSON stream reading in Gson: the first may use a heavy type adapters composition under the hood, whilst the latter simply can parse JSON documents token by token.
Having that said, you can gain a better performance while reading the JSON stream: your JSON file is really known to have a very strict structure so the high-level parser can be implemented much simpler.
Suppose a simple test suite with different implementations for your problem:
Data objects
City.java
final class City {
#SerializedName("_id")
final int id;
#SerializedName("country")
final String country;
#SerializedName("name")
final String name;
#SerializedName("coord")
final Coordinates coordinates;
private City(final int id, final String country, final String name, final Coordinates coordinates) {
this.id = id;
this.country = country;
this.name = name;
this.coordinates = coordinates;
}
static City of(final int id, final String country, final String name, final Coordinates coordinates) {
return new City(id, country, name, coordinates);
}
#Override
public boolean equals(final Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
final City that = (City) o;
return id == that.id;
}
#Override
public int hashCode() {
return id;
}
#SuppressWarnings("ConstantConditions")
public static int compareByName(final City city1, final City city2) {
return city1.name.compareTo(city2.name);
}
}
Coordinates.java
final class Coordinates {
#SerializedName("lat")
final double latitude;
#SerializedName("lon")
final double longitude;
private Coordinates(final double latitude, final double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
static Coordinates of(final double latitude, final double longitude) {
return new Coordinates(latitude, longitude);
}
#Override
public boolean equals(final Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
final Coordinates that = (Coordinates) o;
return Double.compare(that.latitude, latitude) == 0
&& Double.compare(that.longitude, longitude) == 0;
}
#Override
public int hashCode() {
final long latitudeBits = Double.doubleToLongBits(latitude);
final long longitudeBits = Double.doubleToLongBits(longitude);
final int latitudeHash = (int) (latitudeBits ^ latitudeBits >>> 32);
final int longitudeHash = (int) (longitudeBits ^ longitudeBits >>> 32);
return 31 * latitudeHash + longitudeHash;
}
}
Test infrastructure
ITest.java
interface ITest {
#Nonnull
default String getName() {
return getClass().getSimpleName();
}
#Nonnull
Collection<City> test(#Nonnull JsonReader jsonReader)
throws IOException;
}
main
public static void main(final String... args)
throws IOException {
final Iterable<ITest> tests = ImmutableList.of(
FirstTest.get(),
ReadAsWholeListTest.get(),
ReadAsWholeTreeSetTest.get(),
ReadJsonStreamIntoListTest.get(),
ReadJsonStreamIntoTreeSetTest.get(),
ReadJsonStreamIntoListChunksTest.get()
);
for ( int i = 0; i < 3; i++ ) {
for ( final ITest test : tests ) {
try ( final ZipInputStream zipInputStream = new ZipInputStream(Resources.getPackageResourceInputStream(Q49273660.class, "cities.json.zip")) ) {
for ( ZipEntry zipEntry = zipInputStream.getNextEntry(); zipEntry != null; zipEntry = zipInputStream.getNextEntry() ) {
if ( zipEntry.getName().equals("cities.json") ) {
final JsonReader jsonReader = new JsonReader(new InputStreamReader(zipInputStream)); // do not close
System.out.printf("%1$35s : ", test.getName());
final Stopwatch stopwatch = Stopwatch.createStarted();
final Collection<City> cities = test.test(jsonReader);
System.out.printf("in %d ms with %d elements\n", stopwatch.elapsed(TimeUnit.MILLISECONDS), cities.size());
assertSorted(cities, City::compareByName);
}
}
}
}
System.out.println("--------------------");
}
}
private static <E> void assertSorted(final Iterable<? extends E> iterable, final Comparator<? super E> comparator) {
final Iterator<? extends E> iterator = iterable.iterator();
if ( !iterator.hasNext() ) {
return;
}
E a = iterator.next();
if ( !iterator.hasNext() ) {
return;
}
do {
final E b = iterator.next();
if ( comparator.compare(a, b) > 0 ) {
throw new AssertionError(a + " " + b);
}
a = b;
} while ( iterator.hasNext() );
}
Tests
FirstTest.java
This is the slowest one.
And it's just an adaptation of your question to the tests.
final class FirstTest
implements ITest {
private static final ITest instance = new FirstTest();
private FirstTest() {
}
static ITest get() {
return instance;
}
#Nonnull
#Override
public List<City> test(#Nonnull final JsonReader jsonReader)
throws IOException {
jsonReader.beginArray();
final List<City> cities = new ArrayList<>();
while ( jsonReader.hasNext() ) {
final City city = new Gson().fromJson(jsonReader, City.class);
cities.add(city);
}
jsonReader.endArray();
cities.sort(City::compareByName);
return cities;
}
}
ReadAsWholeListTest.java
This is most likely how you might implement it.
It's not the winner, but it's the simplest one, and it uses default sorting.
final class ReadAsWholeListTest
implements ITest {
private static final ITest instance = new ReadAsWholeListTest();
private ReadAsWholeListTest() {
}
static ITest get() {
return instance;
}
private static final Gson gson = new Gson();
private static final Type citiesListType = new TypeToken<List<City>>() {
}.getType();
#Nonnull
#Override
public List<City> test(#Nonnull final JsonReader jsonReader) {
final List<City> cities = gson.fromJson(jsonReader, citiesListType);
cities.sort(City::compareByName);
return cities;
}
}
ReadAsWholeTreeSetTest.java
Another idea, if you're not bound to lists, is using an already-sorted collections like TreeSet.
Since I don't know if there's a way to specify a new TreeSet comparator mechanism in Gson, it must use a custom type adapter factory (but this is not required if City is already comparable by name, however it's not flexible).
final class ReadAsWholeTreeSetTest
implements ITest {
private static final ITest instance = new ReadAsWholeTreeSetTest();
private ReadAsWholeTreeSetTest() {
}
static ITest get() {
return instance;
}
#SuppressWarnings({ "rawtypes", "unchecked" })
private static final TypeToken<TreeSet<?>> rawTreeSetType = (TypeToken) TypeToken.get(TreeSet.class);
private static final Map<Type, Comparator<?>> comparatorsRegistry = ImmutableMap.of(
City.class, (Comparator<City>) City::compareByName
);
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new TypeAdapterFactory() {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !TreeSet.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
final Type elementType = ((ParameterizedType) typeToken.getType()).getActualTypeArguments()[0];
#SuppressWarnings({ "rawtypes", "unchecked" })
final Comparator<Object> comparator = (Comparator) comparatorsRegistry.get(elementType);
if ( comparator == null ) {
return null;
}
final TypeAdapter<TreeSet<?>> originalTreeSetTypeAdapter = gson.getDelegateAdapter(this, rawTreeSetType);
final TypeAdapter<?> originalElementTypeAdapter = gson.getDelegateAdapter(this, TypeToken.get(elementType));
final TypeAdapter<TreeSet<Object>> treeSetTypeAdapter = new TypeAdapter<TreeSet<Object>>() {
#Override
public void write(final JsonWriter jsonWriter, final TreeSet<Object> treeSet)
throws IOException {
originalTreeSetTypeAdapter.write(jsonWriter, treeSet);
}
#Override
public TreeSet<Object> read(final JsonReader jsonReader)
throws IOException {
jsonReader.beginArray();
final TreeSet<Object> elements = new TreeSet<>(comparator);
while ( jsonReader.hasNext() ) {
final Object element = originalElementTypeAdapter.read(jsonReader);
elements.add(element);
}
return elements;
}
}.nullSafe();
#SuppressWarnings({ "rawtypes", "unchecked" })
final TypeAdapter<T> castTreeSetTypeAdapter = (TypeAdapter<T>) treeSetTypeAdapter;
return castTreeSetTypeAdapter;
}
})
.create();
private static final Type citiesSetType = new TypeToken<TreeSet<City>>() {
}.getType();
#Nonnull
#Override
public Set<City> test(#Nonnull final JsonReader jsonReader) {
return gson.fromJson(jsonReader, citiesSetType);
}
}
JSON stream reader tests
The following class is a special reader test that uses a simplified strategy of reading the cities JSON.
AbstractJsonStreamTest.java
It's probably as simplest as possible (in terms of JSON structure analysis), and it requires the JSON document to be very strict.
abstract class AbstractJsonStreamTest
implements ITest {
protected static void read(final JsonReader jsonReader, final Consumer<? super City> cityConsumer)
throws IOException {
jsonReader.beginArray();
while ( jsonReader.hasNext() ) {
jsonReader.beginObject();
require(jsonReader, "country");
final String country = jsonReader.nextString();
require(jsonReader, "name");
final String name = jsonReader.nextString();
require(jsonReader, "_id");
final int id = jsonReader.nextInt();
require(jsonReader, "coord");
jsonReader.beginObject();
require(jsonReader, "lon");
final double longitude = jsonReader.nextDouble();
require(jsonReader, "lat");
final double latitude = jsonReader.nextDouble();
jsonReader.endObject();
jsonReader.endObject();
final City city = City.of(id, country, name, Coordinates.of(latitude, longitude));
cityConsumer.accept(city);
}
jsonReader.endArray();
}
private static void require(final JsonReader jsonReader, final String expectedName)
throws IOException {
final String actualName = jsonReader.nextName();
if ( !actualName.equals(expectedName) ) {
throw new JsonParseException("Expected " + expectedName + " but was " + actualName);
}
}
}
ReadJsonStreamIntoListTest.java
This one is pretty much like ReadAsWholeListTest but it uses a simplified deserialization mechanism.
final class ReadJsonStreamIntoListTest
extends AbstractJsonStreamTest {
private static final ITest instance = new ReadJsonStreamIntoListTest();
private ReadJsonStreamIntoListTest() {
}
static ITest get() {
return instance;
}
#Nonnull
#Override
public Collection<City> test(#Nonnull final JsonReader jsonReader)
throws IOException {
final List<City> cities = new ArrayList<>();
read(jsonReader, cities::add);
cities.sort(City::compareByName);
return cities;
}
}
ReadJsonStreamIntoTreeSetTest.java
This one, like the previous one, is also just another implementation of a more expensive implementation (ReadAsWholeTreeSetTest), however it does not require a custom type adatpter.
final class ReadJsonStreamIntoTreeSetTest
extends AbstractJsonStreamTest {
private static final ITest instance = new ReadJsonStreamIntoTreeSetTest();
private ReadJsonStreamIntoTreeSetTest() {
}
static ITest get() {
return instance;
}
#Nonnull
#Override
public Collection<City> test(#Nonnull final JsonReader jsonReader)
throws IOException {
final Collection<City> cities = new TreeSet<>(City::compareByName);
read(jsonReader, cities::add);
return cities;
}
}
ReadJsonStreamIntoListChunksTest.java
The following test is based on your initial idea, but it does not sort the chunks in parallel (I'm not sure but you could give it a try).
I still think that the previous two are simpler and probably easier to maintain and give more performance gain.
final class ReadJsonStreamIntoListChunksTest
extends AbstractJsonStreamTest {
private static final ITest instance = new ReadJsonStreamIntoListChunksTest();
private ReadJsonStreamIntoListChunksTest() {
}
static ITest get() {
return instance;
}
#Nonnull
#Override
public List<City> test(#Nonnull final JsonReader jsonReader)
throws IOException {
final Collection<List<City>> cityChunks = new ArrayList<>();
final AtomicReference<List<City>> cityChunkRef = new AtomicReference<>(new ArrayList<>());
read(jsonReader, city -> {
final List<City> cityChunk = cityChunkRef.get();
cityChunk.add(city);
if ( cityChunk.size() >= 10000 ) {
cityChunks.add(cityChunk);
cityChunkRef.set(new ArrayList<>());
}
});
if ( !cityChunkRef.get().isEmpty() ) {
cityChunks.add(cityChunkRef.get());
}
for ( final List<City> cities : cityChunks ) {
Collections.sort(cities, City::compareByName);
}
return merge(cityChunks, City::compareByName);
}
/**
* <p>Adapted from:</p>
* <ul>
* <li>Original question: https://stackoverflow.com/questions/1774256/java-code-review-merge-sorted-lists-into-a-single-sorted-list</li>
* <li>Accepted answer: https://stackoverflow.com/questions/1774256/java-code-review-merge-sorted-lists-into-a-single-sorted-list/1775748#1775748</li>
* </ul>
*/
#SuppressWarnings("MethodCallInLoopCondition")
private static <E> List<E> merge(final Iterable<? extends List<E>> lists, final Comparator<? super E> comparator) {
int totalSize = 0;
for ( final List<E> l : lists ) {
totalSize += l.size();
}
final List<E> result = new ArrayList<>(totalSize);
while ( result.size() < totalSize ) { // while we still have something to add
List<E> lowest = null;
for ( final List<E> l : lists ) {
if ( !l.isEmpty() ) {
if ( lowest == null || comparator.compare(l.get(0), lowest.get(0)) <= 0 ) {
lowest = l;
}
}
}
assert lowest != null;
result.add(lowest.get(0));
lowest.remove(0);
}
return result;
}
}
Test results
For my desktop JRE I could obtain the following test results:
FirstTest : in 5797 ms with 209557 elements
ReadAsWholeListTest : in 796 ms with 209557 elements
ReadAsWholeTreeSetTest : in 733 ms with 162006 elements
ReadJsonStreamIntoListTest : in 461 ms with 209557 elements
ReadJsonStreamIntoTreeSetTest : in 452 ms with 162006 elements
ReadJsonStreamIntoListChunksTest : in 607 ms with 209557 elements
--------------------
FirstTest : in 3396 ms with 209557 elements
ReadAsWholeListTest : in 493 ms with 209557 elements
ReadAsWholeTreeSetTest : in 520 ms with 162006 elements
ReadJsonStreamIntoListTest : in 385 ms with 209557 elements
ReadJsonStreamIntoTreeSetTest : in 377 ms with 162006 elements
ReadJsonStreamIntoListChunksTest : in 540 ms with 209557 elements
--------------------
FirstTest : in 3448 ms with 209557 elements
ReadAsWholeListTest : in 429 ms with 209557 elements
ReadAsWholeTreeSetTest : in 421 ms with 162006 elements
ReadJsonStreamIntoListTest : in 400 ms with 209557 elements
ReadJsonStreamIntoTreeSetTest : in 385 ms with 162006 elements
ReadJsonStreamIntoListChunksTest : in 480 ms with 209557 elements
--------------------
As you can see, creating excessive Gson instance is definitely a wrong idea.
The more optimized tests gain better performance.
However, splitting a large list into sorted chunks (no parallel) to be merged later does not give much performance gain in my environment.
For simplicity and probably the best choice, I would go with ReadJsonStreamInto_Collection_Test depending on the required collection.
I'm not really sure how fine would it work in a real Android environment, but you can simply do some JSON deserialization a bit better than Gson does using its internals.
By the way:
I'm not really sure, but did you notice 162006 unique cities? Your JSON file probably has some duplicates (at least if its _id is the identity).
What if you simply generate a sorted version of cities.json in advance on your workstation before you use it on an Android device? Additionally, you might want to filter out the duplicates if my above assumption is correct.
I have requirement where I need to convert java object to json.
I am using Gson for that but i need the converter to only serialize the non null or not empty values.
For example:
//my java object looks like
class TestObject{
String test1;
String test2;
OtherObject otherObject = new OtherObject();
}
now my Gson instance to convert this object to json looks like
Gson gson = new Gson();
TestObject obj = new TestObject();
obj.test1 = "test1";
obj.test2 = "";
String jsonStr = gson.toJson(obj);
println jsonStr;
In the above print, the result is
{"test1":"test1", "test2":"", "otherObject":{}}
Here i just wanted the result to be
{"test1":"test1"}
Since the test2 is empty and otherObject is empty, i don't want them to be serialized to json data.
Btw, I am using Groovy/Grails so if there is any plugin for this that would be good, if not any suggestion to customize the gson serialization class would be good.
Create your own TypeAdapter
public class MyTypeAdapter extends TypeAdapter<TestObject>() {
#Override
public void write(JsonWriter out, TestObject value) throws IOException {
out.beginObject();
if (!Strings.isNullOrEmpty(value.test1)) {
out.name("test1");
out.value(value.test1);
}
if (!Strings.isNullOrEmpty(value.test2)) {
out.name("test2");
out.value(value.test1);
}
/* similar check for otherObject */
out.endObject();
}
#Override
public TestObject read(JsonReader in) throws IOException {
// do something similar, but the other way around
}
}
You can then register it with Gson.
Gson gson = new GsonBuilder().registerTypeAdapter(TestObject.class, new MyTypeAdapter()).create();
TestObject obj = new TestObject();
obj.test1 = "test1";
obj.test2 = "";
System.out.println(gson.toJson(obj));
produces
{"test1":"test1"}
The GsonBuilder class has a bunch of methods to create your own serialization/deserialization strategies, register type adapters, and set other parameters.
Strings is a Guava class. You can do your own check if you don't want that dependency.
What I personally don't like in TypeAdapter using answer is the fact you need to describe every field of your entire class which could have lets say 50 fields (which means 50 if blocks in TypeAdapter).
My solution is based on Reflection and a fact Gson will not serialize null values fields by default.
I have a special class which holds data for API to create document called DocumentModel, which has about 50 fields and I don't like to send String fields with "" (empty but not null) values or empty arrays to server. So I created a special method which returns me a copy of my object with all empty fields nulled. Note - by default all arrays in my DocumentModel instance are initialized as empty (zero length) arrays and thus they are never null, you should probably check your arrays for null before checking their length.
public DocumentModel getSerializableCopy() {
Field fields[] = new Field[]{};
try {
// returns the array of Field objects representing the public fields
fields = DocumentModel.class.getDeclaredFields();
} catch (Exception e) {
e.printStackTrace();
}
DocumentModel copy = new DocumentModel();
Object value;
for (Field field : fields) {
try {
value = field.get(this);
if (value instanceof String && TextUtils.isEmpty((String) value)) {
field.set(copy, null);
// note: here array is not being checked for null!
else if (value instanceof Object[] && ((Object[]) value).length == 0) {
field.set(copy, null);
} else
field.set(copy, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return copy;
}
Using this method I don't care if some fields was added or removed after this method was written or whatever. The only problem left - is checking custom type fields, which are not String or array, but this depends to particular class and should be extra coded in if/else blocks.
It seems to me the problem is not with gson. Gson correctly keeps track of the difference between null and an empty string. Are you sure you want to erase that distinction? Are you sure all classes that use TestObject don't care?
What you could do if you don't care about the difference is to change the empty strings to null within a TestObject before serializing it. Or better, make the setters in TestObject such that an empty string is set to null; that way you define rigidly within the class that an empty string is the same as null. You'll have to make sure the values cannot be set outside the setters.
I have ran into the same problem and found 2 distinct solutions
Write a custom TypeAdapter for each field class
TypeAdapter example for String class:
#SuppressWarnings("rawtypes")
public class JSONStringAdapter extends TypeAdapter {
#Override
public String read(JsonReader jsonReader) throws IOException {
String value = jsonReader.nextString();
if(value == null || value.trim().length() == 0) {
return null;
} else {
return value;
}
}
#Override
public void write(JsonWriter jsonWriter, Object object) throws IOException {
String value = String.valueOf(object);
if(value == null || value.trim().length() == 0) {
jsonWriter.nullValue();
} else {
jsonWriter.value(value);
}
}
}
Use:
public class Doggo {
#JsonAdapter(JSONStringAdapter.class)
private String name;
public Doggo(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Doggo aDoggo = new Doggo("");
String jsonString = new Gson().toJson(aDoggo);
}
}
Process the object manually before generating the JSON string
Seems to work on anything, haven't tested the performance:
public static boolean removeEmpty(JSONObject source) {
if (null == source || source.length() == 0) {
return true;
}
boolean isJsonObjectEmpty = false;
for (String key : JSONObject.getNames(source)) {
Object value = source.get(key);
boolean isValueEmpty = isValueEmpty(value);
if(isValueEmpty) {
source.remove(key);
}
}
if(source.length() == 0) {
isJsonObjectEmpty = true;
}
return isJsonObjectEmpty;
}
private static boolean isValueEmpty(Object value) {
if (null == value) {
return true;
}
if (value instanceof JSONArray) {
JSONArray arr = (JSONArray) value;
if(arr.length() > 0) {
List<Integer> indextesToRemove = new ArrayList<>();
for(int i = 0; i< arr.length(); i++) {
boolean isValueEmpty = isValueEmpty(arr.get(i));
if(isValueEmpty) {
indextesToRemove.add(i);
};
}
for(Integer index : indextesToRemove) {
arr.remove(index);
}
if(arr.length() == 0) {
return true;
}
} else {
return true;
}
} else if (value instanceof JSONObject) {
return removeEmpty((JSONObject) value);
} else {
if (JSONObject.NULL.equals(value)
|| null == value
|| value.toString().trim().length() == 0)
) {
return true;
}
}
return false;
}
Use:
public class Doggo {
private String name;
public Doggo(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Doggo aDoggo = new Doggo("");
// if you are not using Type Adapters for your fields
JSONObject aJSONObject1 = new JSONObject(aDoggo);
removeEmpty(aJSONObject1);
String jsonString1 = aJSONObject1.toString();
// if you are using Type Adapters for your fields
Gson gsonParser = new Gson();
JSONObject aJSONObject2 = new JSONObject(gsonParser .toJson(aDoggo));
removeEmpty(aJSONObject2);
String jsonString2 = aJSONObject2.toString();
}
}
I'm trying to create an abstract class for defining configuration classes. I wish to export and import these classes from and to JSON whenever I want to. I'm trying to achieve this using Gson.
I'm getting an error when writing to JSON that states it:
can't serialize java.lang.Class - Forgot to register a type adapter?
My main class: https://hastebin.com/pogohodovi.scala
Abstract config class: https://hastebin.com/adeyawubuy.cs
An example of a child class:
public class DyescapeCOREConfiguration extends DyescapeConfiguration {
private static transient DyescapeCOREConfiguration i = new DyescapeCOREConfiguration();
public static DyescapeCOREConfiguration get() { return i; }
#Expose public static String ServerID = UUID.randomUUID().toString();
}
Please note: I need to keep the variables in the child configuration classes static. I tried to create some adapters/serializers, but they don't seem to work.
You're probably doing:
gson.toJson(DyescapeCOREConfiguration.class)
In order to serialize this class, you still must create an instance of DyescapeCOREConfiguration. Since statics are not (de)serialized by default, you have to enable them (IMHO, enabling such modifier is really not a good idea):
final Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.excludeFieldsWithModifiers(TRANSIENT) // STATIC|TRANSIENT in the default configuration
.create();
final String json = gson.toJson(new DyescapeCOREConfiguration());
System.out.println(json);
The output:
{"ServerID":"37145480-64b9-4beb-b031-2d619f14a44b"}
Update
If obtaining an instance is not possible for whatever reason, write a custom Class<?> type adapter (I would never use it in practice):
StaticTypeAdapterFactory.java
final class StaticTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory staticTypeAdapterFactory = new StaticTypeAdapterFactory();
private StaticTypeAdapterFactory() {
}
static TypeAdapterFactory getStaticTypeAdapterFactory() {
return staticTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final Type type = typeToken.getType();
if ( type.equals(Class.class) ) {
#SuppressWarnings("unchecked")
final TypeAdapter<T> castStaticTypeAdapter = (TypeAdapter<T>) getStaticTypeAdapter(gson);
return castStaticTypeAdapter;
}
return null;
}
}
StaticTypeAdapter.java
final class StaticTypeAdapter<T>
extends TypeAdapter<Class<T>> {
private static final String TARGET_CLASS_PROPERTY = "___class";
private final Gson gson;
private StaticTypeAdapter(final Gson gson) {
this.gson = gson;
}
static <T> TypeAdapter<Class<T>> getStaticTypeAdapter(final Gson gson) {
return new StaticTypeAdapter<>(gson);
}
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final Class<T> value)
throws IOException {
try {
final Iterator<Field> iterator = Stream.of(value.getFields())
.filter(f -> isStatic(f.getModifiers()))
.iterator();
out.beginObject();
while ( iterator.hasNext() ) {
final Field field = iterator.next();
out.name(field.getName());
field.setAccessible(true);
final Object fieldValue = field.get(null);
#SuppressWarnings({ "unchecked", "rawtypes" })
final TypeAdapter<Object> adapter = (TypeAdapter) gson.getAdapter(field.getType());
adapter.write(out, fieldValue);
}
out.name(TARGET_CLASS_PROPERTY);
out.value(value.getName());
out.endObject();
} catch ( final IllegalAccessException ex ) {
throw new IOException(ex);
}
}
#Override
public Class<T> read(final JsonReader in)
throws IOException {
try {
Class<?> type = null;
in.beginObject();
final Map<String, JsonElement> buffer = new HashMap<>();
while ( in.peek() != END_OBJECT ) {
final String property = in.nextName();
switch ( property ) {
case TARGET_CLASS_PROPERTY:
type = Class.forName(in.nextString());
break;
default:
// buffer until the target class name is known
if ( type == null ) {
final TypeAdapter<JsonElement> adapter = gson.getAdapter(JsonElement.class);
final JsonElement jsonElement = adapter.read(in);
buffer.put(property, jsonElement);
} else {
// flush the buffer
if ( !buffer.isEmpty() ) {
for ( final Entry<String, JsonElement> e : buffer.entrySet() ) {
final Field field = type.getField(e.getKey());
final Object value = gson.getAdapter(field.getType()).read(in);
field.set(null, value);
}
buffer.clear();
}
final Field field = type.getField(property);
if ( isStatic(field.getModifiers()) ) {
final TypeAdapter<?> adapter = gson.getAdapter(field.getType());
final Object value = adapter.read(in);
field.set(null, value);
}
}
break;
}
}
in.endObject();
// flush the buffer
if ( type != null && !buffer.isEmpty() ) {
for ( final Entry<String, JsonElement> e : buffer.entrySet() ) {
final Field field = type.getField(e.getKey());
final Object value = gson.fromJson(e.getValue(), field.getType());
field.set(null, value);
}
buffer.clear();
}
#SuppressWarnings({ "unchecked", "rawtypes" })
final Class<T> castType = (Class) type;
return castType;
} catch ( final ClassNotFoundException | NoSuchFieldException | IllegalAccessException ex ) {
throw new IOException(ex);
}
}
}
Example use:
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getStaticTypeAdapterFactory())
.create();
final String json = gson.toJson(DyescapeCOREConfiguration.class);
out.println("DyescapeCOREConfiguration.ServerID=" + DyescapeCOREConfiguration.ServerID);
// ---
DyescapeCOREConfiguration.ServerID = "whatever";
out.println("DyescapeCOREConfiguration.ServerID=" + DyescapeCOREConfiguration.ServerID);
// ---
#SuppressWarnings("unchecked")
final Class<DyescapeCOREConfiguration> configurationClass = gson.fromJson(json, Class.class);
// ^--- this is awful, omitting a useless assignment is even worse
out.println("DyescapeCOREConfiguration.ServerID=" + DyescapeCOREConfiguration.ServerID);
Output:
DyescapeCOREConfiguration.ServerID=012fa795-abd8-4b91-b6f5-bab67f73ae17
DyescapeCOREConfiguration.ServerID=whatever
DyescapeCOREConfiguration.ServerID=012fa795-abd8-4b91-b6f5-bab67f73ae17
However, I still recommend you to avoid the idea of static fields (de)serialization.