Assuming the following enum in a Java class:
enum AccessMode {
READ_WRITE,
READ_ONLY,
WRITE_ONLY
};
JSON deserialization works fine with Gson as long as the JSON contains a valid value for the enum field, e.g.:
"access": "READ_WRITE"
Unfortunately, fromJson() does seem to detect invalid enum values in the JSON, such as:
"access": "READ_XXX"
How can I add enum value checking when deserializing a JSON file using Gson?
As of version 2.8.2, Gson does not support such a use case.
I believe it's worthy to be submitted as a suggestion to the Gson development team as a special GsonBuilder configuration method.
The most you can do now is writing a custom enum type adapter that almost duplicates com.google.gson.internal.bind.EnumTypeAdapter functionality but adds the name check.
final class StrictEnumTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory allStrictEnumTypeAdapterFactory = new StrictEnumTypeAdapterFactory(enumClass -> true);
private final Predicate<? super Class<? extends Enum<?>>> isStrictEnumClass;
private StrictEnumTypeAdapterFactory(final Predicate<? super Class<? extends Enum<?>>> isStrictEnumClass) {
this.isStrictEnumClass = isStrictEnumClass;
}
static TypeAdapterFactory get(final Predicate<? super Class<? extends Enum<?>>> isStrictEnumClass) {
return new StrictEnumTypeAdapterFactory(isStrictEnumClass);
}
static TypeAdapterFactory get() {
return allStrictEnumTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final Class<? super T> rawType = typeToken.getRawType();
// Skip non-enums
if ( !Enum.class.isAssignableFrom(rawType) ) {
return null;
}
// Check if the enum is supported by the "strict" policy
#SuppressWarnings("unchecked")
final Class<? extends Enum<?>> enumRawType = (Class<? extends Enum<?>>) rawType;
if ( !isStrictEnumClass.test(enumRawType) ) {
return null;
}
// Trivial rawtypes/unchecked casts
#SuppressWarnings({ "rawtypes", "unchecked" })
final TypeAdapter<? extends Enum<?>> strictEnumTypeAdapter = StrictEnumTypeAdapter.get((Class) enumRawType);
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) strictEnumTypeAdapter;
return castTypeAdapter;
}
private static final class StrictEnumTypeAdapter<E extends Enum<E>>
extends TypeAdapter<E> {
private final Class<E> enumClass;
private final Map<String, E> nameToEnumConstant;
private final Map<E, String> enumConstantToName;
private StrictEnumTypeAdapter(final Class<E> enumClass, final Map<String, E> nameToEnumConstant, final Map<E, String> enumConstantToName) {
this.enumClass = enumClass;
this.nameToEnumConstant = nameToEnumConstant;
this.enumConstantToName = enumConstantToName;
}
private static <E extends Enum<E>> TypeAdapter<E> get(final Class<E> enumClass) {
final Map<String, E> nameToEnumConstant = new HashMap<>();
final Map<E, String> enumConstantToName = new HashMap<>();
final Map<String, E> enumNameToEnumConstant = Stream.of(enumClass.getEnumConstants())
.collect(Collectors.toMap(Enum::name, Function.identity()));
Stream.of(enumClass.getFields())
// It can be either a simple enum constant, or an enum constant that overrides
.filter(field -> enumClass.isAssignableFrom(field.getType()))
.forEach(field -> {
final E enumConstant = enumNameToEnumConstant.get(field.getName());
// For compatibility with the original type adapter, we have to respect the #SeriaizedName annotation
final SerializedName serializedName = field.getAnnotation(SerializedName.class);
if ( serializedName == null ) {
nameToEnumConstant.put(field.getName(), enumConstant);
enumConstantToName.put(enumConstant, field.getName());
} else {
nameToEnumConstant.put(serializedName.value(), enumConstant);
enumConstantToName.put(enumConstant, serializedName.value());
for ( final String alternate : serializedName.alternate() ) {
nameToEnumConstant.put(alternate, enumConstant);
}
}
});
return new StrictEnumTypeAdapter<>(enumClass, nameToEnumConstant, enumConstantToName)
.nullSafe(); // A convenient method to handle nulls
}
#Override
public void write(final JsonWriter out, final E value)
throws IOException {
out.value(enumConstantToName.get(value));
}
#Override
public E read(final JsonReader in)
throws IOException {
final String key = in.nextString();
// This is what the original type adapter probably misses
if ( !nameToEnumConstant.containsKey(key) ) {
throw new JsonParseException(enumClass + " does not have an enum named " + key + " at " + in);
}
return nameToEnumConstant.get(key);
}
}
}
Simple test:
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(StrictEnumTypeAdapterFactory.get())
.create();
public static void main(final String... args)
throws IOException {
try ( final JsonReader jsonReader = Resources.getPackageResourceJsonReader(Q49572505.class, "good.json") ) {
System.out.println(gson.<Status>fromJson(jsonReader, Status.class).access);
}
try ( final JsonReader jsonReader = Resources.getPackageResourceJsonReader(Q49572505.class, "bad.json") ) {
try {
gson.<Status>fromJson(jsonReader, Status.class);
throw new AssertionError();
} catch ( final JsonParseException ex ) {
System.out.println(ex.getMessage());
}
}
}
Output:
READ_WRITE
class q49572505.AccessMode does not have an enum named READ_XXX at JsonReader at line 2 column 22 path $.access
You could look # Moshi. I have found it a suitable and straightforward replacement for GSON and it already supports this behavior.
#Lyubomyr_Shaydarlv's solution works, but if you don't want to duplicate GSON's internal code, you can use it as a delegate in a custom TypeAdapterFactory. Run the adapter, and if it returns null, you know the value was invalid. This has the advantage that it inherits and changes to default enum converter.
class StrictEnumTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<T> rawType = (Class<T>) type.getRawType();
if (!rawType.isEnum()) {
return null;
}
return newStrictEnumAdapter(gson.getDelegateAdapter(this, type));
}
private <T> TypeAdapter<T> newStrictEnumAdapter(
final TypeAdapter<T> delegateAdapter) {
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
delegateAdapter.write(out, value);
}
#Override
public T read(JsonReader in) throws IOException {
// Peek at the next value and save it for the error message
// if you don't need the offending value's actual name
String enumValue = in.nextString();
JsonReader delegateReader = new JsonReader(new StringReader('"' + enumValue + '"'));
T value = delegateAdapter.read(delegateReader);
delegateReader.close();
if (value == null) throw new IllegalStateException("Invalid enum value - " + enumValue);
return value;
}
};
}
}
Related
Given a class that I cannot modifiy
class ThirdPartyDTO
{
Instant foo;
Instant bar;
// many more fields.
}
I have a JSON representation of the class that uses two diferent patterns to represent foo and bar.
If the field name is foo, use this pattern, if the field name is bar, use the other pattern.
How can I do this with gson without adding (because I can't) annotations on each field name?
Thanks.
So, as I mentioned in the comments above, Gson type adapters do not have access to the full context of the objects they serialize or deserialize. For example, a type adapter for a single type (hierarchy) does not really know what field it may be applied to (and this is the problem in the post). In order to apply different type adapters for different fields, JsonSerializer and JsonDeserializer can be used (therefore every field must be processed manually that is a tedious job). Another bad thing here is that the ReflectiveTypeAdapterFactory that is supposed to process DTOs like that is not extensible directly but can only be extended via the GsonBuilder interface that is also limited.
However, it is possible to implement a workaround that uses the following algorithm:
create an exclusion strategy that always skips special fields on deserialization (this affects the ReflectiveTypeAdapterFactory only);
create a type adapter factory that creates type adapters for such special fields;
once Gson deserializes the wrapper object, the special fields in the wrapper object are supposed to be skipped but set to null (other other defaults in case of primitives), the post-deserializer type adapter asks injected strategies to deserialize each special field that was previously skipped by the exclusion strategy hence ReflectiveTypeAdapterFactory.
That's the trick.
interface IPostPatchFactory {
#Nonnull
TypeAdapterFactory createTypeAdapterFactory();
#Nonnull
ExclusionStrategy createExclusionStrategy();
}
#AllArgsConstructor(access = AccessLevel.PRIVATE)
final class PostPatchFactory
implements IPostPatchFactory {
private final Predicate<? super FieldDatum> isFieldPostPatched;
private final Predicate<? super Class<?>> isClassPostPatched;
private final Iterable<FieldPatch<?>> fieldPatches;
static IPostPatchFactory create(final Collection<FieldPatch<?>> fieldPatches) {
final Collection<FieldPatch<?>> fieldPatchesCopy = new ArrayList<>(fieldPatches);
final Collection<Field> postPatchedFields = fieldPatches.stream()
.map(FieldPatch::getField)
.collect(Collectors.toList());
final Collection<FieldDatum> postPatchedFieldAttributes = postPatchedFields.stream()
.map(FieldDatum::from)
.collect(Collectors.toList());
final Collection<? super Class<?>> isClassPostPatched = postPatchedFieldAttributes.stream()
.map(fieldDatum -> fieldDatum.declaringClass)
.collect(Collectors.toList());
return new PostPatchFactory(postPatchedFieldAttributes::contains, isClassPostPatched::contains, fieldPatchesCopy);
}
#Nonnull
#Override
public TypeAdapterFactory createTypeAdapterFactory() {
return new PostPatchTypeAdapterFactory(isClassPostPatched, fieldPatches);
}
#Nonnull
#Override
public ExclusionStrategy createExclusionStrategy() {
return new PostPatchExclusionStrategy(isFieldPostPatched);
}
#AllArgsConstructor(access = AccessLevel.PRIVATE)
private static final class PostPatchTypeAdapterFactory
implements TypeAdapterFactory {
private final Predicate<? super Class<?>> isClassPostPatched;
private final Iterable<FieldPatch<?>> fieldPatches;
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final Class<? super T> rawType = typeToken.getRawType();
if ( !isClassPostPatched.test(rawType) ) {
return null;
}
return new PostPatchTypeAdapter<>(gson, gson.getDelegateAdapter(this, typeToken), fieldPatches)
.nullSafe();
}
#AllArgsConstructor(access = AccessLevel.PRIVATE)
private static final class PostPatchTypeAdapter<T>
extends TypeAdapter<T> {
private final Gson gson;
private final TypeAdapter<T> delegateTypeAdapter;
private final Iterable<FieldPatch<?>> fieldPatches;
#Override
public void write(final JsonWriter out, final T value) {
throw new UnsupportedOperationException("TODO");
}
#Override
public T read(final JsonReader in) {
final JsonElement bufferedJsonElement = JsonParser.parseReader(in);
final T value = delegateTypeAdapter.fromJsonTree(bufferedJsonElement);
for ( final FieldPatch<?> fieldPatch : fieldPatches ) {
final Field field = fieldPatch.getField();
final BiFunction<? super Gson, ? super JsonElement, ?> deserialize = fieldPatch.getDeserialize();
final Object fieldValue = deserialize.apply(gson, bufferedJsonElement);
try {
field.set(value, fieldValue);
} catch ( final IllegalAccessException ex ) {
throw new RuntimeException(ex);
}
}
return value;
}
}
}
private static final class PostPatchExclusionStrategy
implements ExclusionStrategy {
private final Predicate<? super FieldDatum> isFieldPostPatched;
private PostPatchExclusionStrategy(final Predicate<? super FieldDatum> isFieldPostPatched) {
this.isFieldPostPatched = isFieldPostPatched;
}
#Override
public boolean shouldSkipField(final FieldAttributes fieldAttributes) {
return isFieldPostPatched.test(FieldDatum.from(fieldAttributes));
}
#Override
public boolean shouldSkipClass(final Class<?> clazz) {
return false;
}
}
#AllArgsConstructor(access = AccessLevel.PRIVATE)
#EqualsAndHashCode
private static final class FieldDatum {
private final Class<?> declaringClass;
private final String name;
private static FieldDatum from(final Member member) {
return new FieldDatum(member.getDeclaringClass(), member.getName());
}
private static FieldDatum from(final FieldAttributes fieldAttributes) {
return new FieldDatum(fieldAttributes.getDeclaringClass(), fieldAttributes.getName());
}
}
}
#AllArgsConstructor(staticName = "of")
#Getter
final class FieldPatch<T> {
private final Field field;
private final BiFunction<? super Gson, ? super JsonElement, ? extends T> deserialize;
}
The unit test:
#AllArgsConstructor(access = AccessLevel.PACKAGE)
#EqualsAndHashCode
#ToString
final class ThirdPartyDTO {
private final Instant foo;
private final Instant bar;
}
public final class PostPatchFactoryTest {
private static final Collection<FieldPatch<?>> fieldPatches;
static {
try {
final Field thirdPartyDtoFooField = ThirdPartyDTO.class.getDeclaredField("foo");
thirdPartyDtoFooField.setAccessible(true);
final Field thirdPartyDtoBarField = ThirdPartyDTO.class.getDeclaredField("bar");
thirdPartyDtoBarField.setAccessible(true);
fieldPatches = ImmutableList.<FieldPatch<?>>builder()
.add(FieldPatch.of(thirdPartyDtoFooField, (gson, jsonElement) -> {
final String rawValue = jsonElement.getAsJsonObject()
.get("foo")
.getAsString();
return Instant.parse(rawValue);
}))
.add(FieldPatch.of(thirdPartyDtoBarField, (gson, jsonElement) -> {
final String rawValue = new StringBuilder(jsonElement.getAsJsonObject()
.get("bar")
.getAsString()
)
.reverse()
.toString();
return Instant.parse(rawValue);
}))
.build();
} catch ( final NoSuchFieldException ex ) {
throw new AssertionError(ex);
}
}
private static final IPostPatchFactory unit = PostPatchFactory.create(fieldPatches);
private static final Gson gson = new GsonBuilder()
.disableInnerClassSerialization()
.disableHtmlEscaping()
.addDeserializationExclusionStrategy(unit.createExclusionStrategy())
.registerTypeAdapterFactory(unit.createTypeAdapterFactory())
.create();
#Test
public void test()
throws IOException {
final ThirdPartyDTO expected = new ThirdPartyDTO(Instant.ofEpochSecond(0), Instant.ofEpochSecond(0));
try ( final JsonReader jsonReader = new JsonReader(new InputStreamReader(PostPatchFactoryTest.class.getResourceAsStream("input.json"))) ) {
final ThirdPartyDTO actual = gson.fromJson(jsonReader, ThirdPartyDTO.class);
Assertions.assertEquals(expected, actual);
}
}
}
{
"foo": "1970-01-01T00:00:00Z",
"bar": "Z00:00:00T10-10-0791"
}
(for simplicity, the bar is simply a reversed string to make it obscure for Java format pattern, but make the test more robust)
Note that this approach is generic (and may fit any other type other than Instant), requires JSON trees to be buffered in memory when deserializing classes that contain special fields (built-in JsonSerializer and JsonDeserializer do the same so who cares?), and lose some special support for #SerializedName, #JsonAdapter, etc.
Gson doesnt have direct support for serializing #AutoValue classes or for Optional<> fields, but com.ryanharter.auto.value adds #AutoValue and net.dongliu:gson-java8-datatype adds Optional<> and other java8 types.
However, they dont work together AFAICT.
Test code:
public class TestOptionalWithAutoValue {
private static final Gson gson = new GsonBuilder().serializeNulls()
// doesnt matter which order these are registered in
.registerTypeAdapterFactory(new GsonJava8TypeAdapterFactory())
.registerTypeAdapterFactory(AutoValueGsonTypeAdapterFactory.create())
.create();
#Test
public void testAutoValueOptionalEmpty() {
AvoTestClass subject = AvoTestClass.create(Optional.empty());
String json = gson.toJson(subject, AvoTestClass.class);
System.out.printf("Json produced = %s%n", json);
AvoTestClass back = gson.fromJson(json, new TypeToken<AvoTestClass>() {}.getType());
assertThat(back).isEqualTo(subject);
}
#Test
public void testAutoValueOptionalFull() {
AvoTestClass subject = AvoTestClass.create(Optional.of("ok"));
String json = gson.toJson(subject, AvoTestClass.class);
System.out.printf("Json produced = '%s'%n", json);
AvoTestClass back = gson.fromJson(json, new TypeToken<AvoTestClass>() {}.getType());
assertThat(back).isEqualTo(subject);
}
}
#AutoValue
public abstract class AvoTestClass {
abstract Optional<String> sval();
public static AvoTestClass create(Optional<String> sval) {
return new AutoValue_AvoTestClass(sval);
}
public static TypeAdapter<AvoTestClass> typeAdapter(Gson gson) {
return new AutoValue_AvoTestClass.GsonTypeAdapter(gson);
}
}
#GsonTypeAdapterFactory
public abstract class AutoValueGsonTypeAdapterFactory implements TypeAdapterFactory {
public static TypeAdapterFactory create() {
return new AutoValueGson_AutoValueGsonTypeAdapterFactory();
}
}
gradle dependencies:
annotationProcessor "com.google.auto.value:auto-value:1.7.4"
annotationProcessor("com.ryanharter.auto.value:auto-value-gson-extension:1.3.1")
implementation("com.ryanharter.auto.value:auto-value-gson-runtime:1.3.1")
annotationProcessor("com.ryanharter.auto.value:auto-value-gson-factory:1.3.1")
implementation 'net.dongliu:gson-java8-datatype:1.1.0'
Fails with:
Json produced = {"sval":null}
...
java.lang.NullPointerException: Null sval
...
net.dongliu.gson.OptionalAdapter is called on serialization, but not deserialization.
Im wondering if theres a workaround, or if the answer is that Gson needs to have direct support for Optional<> ?
Glad to see you've updated your question by adding much more information and even by adding a test! :) That really makes it clear!
I'm not sure, but the generated type adapter has no mention for the default value for sval:
jsonReader.beginObject();
// [NOTE] This is where it is initialized with null, so I guess it will definitely fail if the `sval` property is not even present in the deserialized JSON object
Optional<String> sval = null;
while (jsonReader.hasNext()) {
String _name = jsonReader.nextName();
// [NOTE] This is where it skips `null` value so it even does not reach to the `OptionalAdapter` run
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
continue;
}
switch (_name) {
default: {
if ("sval".equals(_name)) {
TypeAdapter<Optional<String>> optional__string_adapter = this.optional__string_adapter;
if (optional__string_adapter == null) {
this.optional__string_adapter = optional__string_adapter = (TypeAdapter<Optional<String>>) gson.getAdapter(TypeToken.getParameterized(Optional.class, String.class));
}
sval = optional__string_adapter.read(jsonReader);
continue;
}
jsonReader.skipValue();
}
}
}
jsonReader.endObject();
return new AutoValue_AvoTestClass(sval);
I have no idea if there is a way to configure the default values in AutoValue or other generators you mentioned, but it looks like a bug.
If there is no any way to work around it (say, library development abandoned; it takes too much time to wait for a fix; whatever), you can always implement it yourself, however with some runtime cost (basically this how Gson works under the hood for data bag objects).
The idea is delegating the job to the built-in RuntimeTypeAdapterFactory so that it could deal with a concrete class, not an abstract one, and set all fields according to the registered type adapters (so that the Java 8 types are supported as well).
The cost here is reflection, thus that adapter may work slower than generated type adapters.
Another thing is that if a JSON property does not even encounter in the JSON object, the corresponding field will remain null.
This requires another post-deserialization type adapter.
final class SubstitutionTypeAdapterFactory
implements TypeAdapterFactory {
private final Function<? super Type, ? extends Type> substitute;
private SubstitutionTypeAdapterFactory(final Function<? super Type, ? extends Type> substitute) {
this.substitute = substitute;
}
static TypeAdapterFactory create(final Function<? super Type, ? extends Type> substitute) {
return new SubstitutionTypeAdapterFactory(substitute);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
#Nullable
final Type substitution = substitute.apply(typeToken.getType());
if ( substitution == null ) {
return null;
}
#SuppressWarnings("unchecked")
final TypeAdapter<T> delegateTypeAdapter = (TypeAdapter<T>) gson.getDelegateAdapter(this, TypeToken.get(substitution));
return delegateTypeAdapter;
}
}
final class DefaultsTypeAdapterFactory
implements TypeAdapterFactory {
private final Function<? super Type, ? extends Type> substitute;
private final LoadingCache<Class<?>, Collection<Map.Entry<Field, ?>>> fieldsCache;
private DefaultsTypeAdapterFactory(final Function<? super Type, ? extends Type> substitute, final Function<? super Type, ?> toDefault) {
this.substitute = substitute;
fieldsCache = CacheBuilder.newBuilder()
// TODO tweak the cache
.build(new CacheLoader<Class<?>, Collection<Map.Entry<Field, ?>>>() {
#Override
public Collection<Map.Entry<Field, ?>> load(final Class<?> clazz) {
// TODO walk hieararchy
return Stream.of(clazz.getDeclaredFields())
.map(field -> {
#Nullable
final Object defaultValue = toDefault.apply(field.getGenericType());
if ( defaultValue == null ) {
return null;
}
field.setAccessible(true);
return new AbstractMap.SimpleImmutableEntry<>(field, defaultValue);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
});
}
static TypeAdapterFactory create(final Function<? super Type, ? extends Type> substitute, final Function<? super Type, ?> toDefault) {
return new DefaultsTypeAdapterFactory(substitute, toDefault);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
#Nullable
final Type substitution = substitute.apply(typeToken.getType());
if ( substitution == null ) {
return null;
}
if ( !(substitution instanceof Class) ) {
return null;
}
final Collection<Map.Entry<Field, ?>> fieldsToPatch = fieldsCache.getUnchecked((Class<?>) substitution);
if ( fieldsToPatch.isEmpty() ) {
return null;
}
final TypeAdapter<T> delegateTypeAdapter = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegateTypeAdapter.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
final T value = delegateTypeAdapter.read(in);
for ( final Map.Entry<Field, ?> e : fieldsToPatch ) {
final Field field = e.getKey();
final Object defaultValue = e.getValue();
try {
if ( field.get(value) == null ) {
field.set(value, defaultValue);
}
} catch ( final IllegalAccessException ex ) {
throw new RuntimeException(ex);
}
}
return value;
}
};
}
}
#AutoValue
abstract class AvoTestClass {
abstract Optional<String> sval();
static AvoTestClass create(final Optional<String> sval) {
return new AutoValue_AvoTestClass(sval);
}
static Class<? extends AvoTestClass> type() {
return AutoValue_AvoTestClass.class;
}
}
public final class OptionalWithAutoValueTest {
private static final Map<Type, Type> autoValueClasses = ImmutableMap.<Type, Type>builder()
.put(AvoTestClass.class, AvoTestClass.type())
.build();
private static final Map<Class<?>, ?> defaultValues = ImmutableMap.<Class<?>, Object>builder()
.put(Optional.class, Optional.empty())
.build();
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new GsonJava8TypeAdapterFactory())
.registerTypeAdapterFactory(SubstitutionTypeAdapterFactory.create(autoValueClasses::get))
.registerTypeAdapterFactory(DefaultsTypeAdapterFactory.create(autoValueClasses::get, type -> {
if ( type instanceof Class ) {
return defaultValues.get(type);
}
if ( type instanceof ParameterizedType ) {
return defaultValues.get(((ParameterizedType) type).getRawType());
}
return null;
}))
.create();
#SuppressWarnings("unused")
private static Stream<Optional<String>> test() {
return Stream.of(
Optional.of("ok"),
Optional.empty()
);
}
#ParameterizedTest
#MethodSource
public void test(final Optional<String> optional) {
final AvoTestClass before = AvoTestClass.create(optional);
final String json = gson.toJson(before, AvoTestClass.class);
final AvoTestClass after = gson.fromJson(json, AvoTestClass.class);
Assert.assertEquals(before, after);
}
}
This solution is reflection-based heavily, but it's just a work-around if the generators cannot do the job (again, not sure if they can be configured so that there are no such issues).
Does Gson have a way to read in non-standard JSON files?
Instead of a typical file like:
[{obj1},{objN}]
I have a file like this:
{obj1}
{objN}
Where there are no square brackets or commas and each object is separated by a newline character.
Yes, it has. Gson supports lenient reading. For example, the following JSON document (non-standard.json):
{
"foo": 1
}
{
"bar": 1
}
you can use the following reading way:
private static final Gson gson = new Gson();
private static final TypeAdapter<JsonElement> jsonElementTypeAdapter = gson.getAdapter(JsonElement.class);
public static void main(final String... args)
throws IOException {
try ( final Reader reader = getPackageResourceReader(Q43528208.class, "non-standard.json") ) {
final JsonReader jsonReader = new JsonReader(reader);
jsonReader.setLenient(true); // this makes it work
while ( jsonReader.peek() != END_DOCUMENT ) {
final JsonElement jsonElement = jsonElementTypeAdapter.read(jsonReader);
System.out.println(jsonElement);
}
}
}
Output:
{"foo":1}
{"bar":1}
I'm not sure if you can write a robust deserializer this way though.
Update
In order to simplify the Gson support, we can implement a few convenient reading methods:
// A shortcut method for the below implementation: aggregates the whole result into a single list
private static <T> List<T> parseToListLenient(final JsonReader jsonReader, final IMapper<? super JsonReader, ? extends T> mapper)
throws IOException {
final List<T> list = new ArrayList<>();
parseLenient(jsonReader, in -> list.add(mapper.map(in)));
return list;
}
// A convenient strategy-accepting method to configure a JsonReader instance to make it lenient and do read
// The consumer defines the strategy what to do with the current JsonReader token
private static void parseLenient(final JsonReader jsonReader, final IConsumer<? super JsonReader> consumer)
throws IOException {
final boolean isLenient = jsonReader.isLenient();
try {
jsonReader.setLenient(true);
while ( jsonReader.peek() != END_DOCUMENT ) {
consumer.accept(jsonReader);
}
} finally {
jsonReader.setLenient(isLenient);
}
}
// Since Java 8 Consumer inteface does not allow checked exceptions to be rethrown
private interface IConsumer<T> {
void accept(T value)
throws IOException;
}
private interface IMapper<T, R> {
R map(T value)
throws IOException;
}
Then simple reading is really simple, and we can just use the methods above:
final Gson gson = new Gson();
final TypeToken<Map<String, Integer>> typeToken = new TypeToken<Map<String, Integer>>() {
};
final TypeAdapter<Map<String, Integer>> typeAdapter = gson.getAdapter(typeToken);
try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43528208.class, "non-standard.json") ) {
final List<Map<String, Integer>> maps = parseToListLenient(jsonReader, typeAdapter::read);
System.out.println(maps);
}
Deserialization via Gson directly would require more complicated implementation:
// This is just a marker not meant to be instantiated but to create a sort of "gateway" to dispatch types in Gson
#SuppressWarnings("unused")
private static final class LenientListMarker<T> {
private LenientListMarker() {
throw new AssertionError("must not be instantiated");
}
}
private static void doDeserialize()
throws IOException {
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new TypeAdapterFactory() {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if the given type is the lenient list marker class
if ( !LenientListMarker.class.isAssignableFrom(typeToken.getRawType()) ) {
// Not the case? Just delegate the job to Gson
return null;
}
final Type listElementType = getTypeParameter0(typeToken.getType());
final TypeAdapter<?> listElementAdapter = gson.getAdapter(TypeToken.get(listElementType));
#SuppressWarnings("unchecked")
final TypeToken<List<?>> listTypeToken = (TypeToken<List<?>>) TypeToken.getParameterized(List.class, listElementType);
final TypeAdapter<List<?>> listAdapter = gson.getAdapter(listTypeToken);
final TypeAdapter<List<?>> typeAdapter = new TypeAdapter<List<?>>() {
#Override
public void write(final JsonWriter out, final List<?> value)
throws IOException {
// Always write a well-formed list
listAdapter.write(out, value);
}
#Override
public List<?> read(final JsonReader in)
throws IOException {
// Delegate the job to the reading method - we only have to tell how to obtain the list values
return parseToListLenient(in, listElementAdapter::read);
}
};
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
return castTypeAdapter;
}
// A simple method to resolve actual type parameter
private Type getTypeParameter0(final Type type) {
if ( !(type instanceof ParameterizedType) ) {
// List or List<?>
return Object.class;
}
return ((ParameterizedType) type).getActualTypeArguments()[0];
}
})
.create();
// This type declares a marker specialization to be used during deserialization
final Type type = new TypeToken<LenientListMarker<Map<String, Integer>>>() {
}.getType();
try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43528208.class, "non-standard.json") ) {
// This is where we're a sort of cheating:
// We tell Gson to deserialize LenientListMarker<Map<String, Integer>> but the type adapter above will return a list
final List<Map<String, Integer>> maps = gson.fromJson(jsonReader, type);
System.out.println(maps);
}
}
The output is now for Map<String, Integer>s, not JsonElements:
[{foo=1}, {bar=1}]
Update 2
TypeToken.getParameterized workaround:
#SuppressWarnings("unchecked")
final TypeToken<List<?>> listTypeToken = (TypeToken<List<?>>) TypeToken.get(new ParameterizedType() {
#Override
public Type getRawType() {
return List.class;
}
#Override
public Type[] getActualTypeArguments() {
return new Type[]{ listElementType };
}
#Override
public Type getOwnerType() {
return null;
}
});
We can have one more program to introduce comma(,) and construct a well formed JSON
With spark 2, we can add multiline as read option.
spark.df.option("multiline","true").json("data.json")
I need to output enum values using Gson which due to client limitations need to be in lower case.
For example CLOSE_FILE would be close_file.
Is there a simple way of doing this? I have looked at making a class which implements JsonSerializer but it looks like I would have to manually serialize the whole class (which is quite complex) is this the case?
If you have control over the enum type, annotate its members with #SerializedName and give it the appropriate serialized value. For example,
enum Action {
#SerializedName("close_file")
CLOSE_FILE;
}
If you don't have control over the enum, provide a custom TypeAdapter when creating a Gson instance. For example,
Gson gson = new GsonBuilder().registerTypeAdapter(Action.class, new TypeAdapter<Action>() {
#Override
public void write(JsonWriter out, Action value) throws IOException {
out.value(value.name().toLowerCase());
}
#Override
public Action read(JsonReader in) throws IOException {
return Action.valueOf(in.nextString().toUpperCase());
}
}).create();
If you want to serialize all enum to lowercase, you can use this code
Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(Enum.class, new EnumToLowerCaseJsonConverter()).create();
public final class EnumToLowerCaseJsonConverter implements JsonSerializer<Enum<?>>, JsonDeserializer<Enum<?>> {
private static Map<String, Class<? extends Enum<?>>> typesToClass = new HashMap<>();
#Override
public JsonElement serialize(final Enum<?> src, final Type typeOfSrc,
final JsonSerializationContext context) {
if (src == null) {
return JsonNull.INSTANCE;
}
return new JsonPrimitive(src.name().toLowerCase());
}
#SuppressWarnings("unchecked")
#Override
public Enum<?> deserialize(final JsonElement json, final Type typeOfT,
final JsonDeserializationContext context) throws JsonParseException {
if (json == null || json.isJsonNull()) {
return null;
}
if (!json.isJsonPrimitive() || !json.getAsJsonPrimitive().isString()) {
throw new JsonParseException(
"Expecting a String JsonPrimitive, getting " + json.toString());
}
try {
final String enumClassName = typeOfT.getTypeName();
Class<? extends Enum<?>> clazz = typesToClass.get(enumClassName);
if (clazz == null) {
clazz = (Class<? extends Enum<?>>) Class.forName(enumClassName);
typesToClass.put(enumClassName, clazz);
}
return Enum.valueOf((Class) clazz, json.getAsString().toUpperCase());
} catch (final ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
}
An easy to use specific type adapter:
Gson gson = new GsonBuilder().registerTypeAdapter(Action.class, new EnumToLowerCaseTypeAdapter<>(Action.class)).create();
public final class EnumToLowerCaseTypeAdapter<T extends Enum<?>> extends TypeAdapter<T> {
private final Class<T> clazz;
public EnumToLowerCaseTypeAdapter(final Class<T> clazz) {
this.clazz = clazz;
}
#Override
public void write(final JsonWriter out, final T value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(value.name().toLowerCase());
}
}
#SuppressWarnings("unchecked")
#Override
public T read(final JsonReader in) throws IOException {
switch (in.peek()) {
case NULL:
in.nextNull();
return null;
default:
final String value = in.nextString();
return (T) Enum.valueOf((Class) clazz, value.toUpperCase());
}
}
}
I have the following code:
public static class A
{
public A() {}
private List<B> bs = new ArrayList<B>();
public List<B> getBs() {
return bs;
}
public void setBs(List<B> bs) {
this.bs = bs;
}
}
public static class B
{
B(String foo){this.foo=foo;}
private String foo;
public String getFoo() {
return foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
}
public static void main(String[] args) throws Exception {
Gson gson = new Gson();
A a = new A();
a.getBs().add(new B("bar"));
System.out.println(gson.toJson(a));
}
and as expected the output is:
{"bs":[{"foo":"bar"}]}
However, if I make A a subclass of HashMap:
public static class A extends HashMap
I get an empty set returned: {}
I have even tried:
System.out.println(gson.toJson(a, new TypeToken<A>(){}.getType()));
and:
System.out.println(gson.toJson(a, new TypeToken<HashMap>(){}.getType()));
Can someone tell me whether/how I can serialise this HashMap subclass using GSON?
Gson works with (default and custom) TypeAdapterFactory instances and the TypeAdapter objects they create to serialize/deserialize your objects.
It goes through the list of registered TypeAdapterFactory objects and picks the first one that can create an appropriate TypeAdapter for the type of the object your are providing. One of these TypeAdapterFactory objects, is one of type MapTypeAdapterFactory which creates a TypeAdapter (of type MapTypeAdapterFactory$Adapter) that serializes/deserializes based on the java.util.Map interface (keys/values). It does nothing about your custom sub type's fields.
If you want Gson to serialize your type as both a Map and a custom type, you will need to register either a custom TypeAdapter directly or a custom TypeAdapterFactory that creates TypeAdapter objects.
Here is the custom TypeAdapterFactory.
Test:
public static void main(String[] args) throws Exception{
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new RetainFieldMapFactory())
.create();
Foo f = gson.fromJson("{'key1':'value1','key2':'value2'}", Foo.class);
System.out.println("in map:\t" + f.toString());
System.out.println("f.key1:\t"+f.key1);
System.out.println("toJson:\t"+gson.toJson(f));
}
public static class Foo extends HashMap<String, String> {
private String key1;
}
Output:
in map: {key2=value2}
f.key1: value1
toJson: {"key2":"value2","key1":"value1"}
RetainFieldMapFactory.java:
/**
* Created by linfaxin on 2015/4/9 009.
* Email: linlinfaxin#163.com
*/
public class RetainFieldMapFactory implements TypeAdapterFactory {
FieldNamingPolicy fieldNamingPolicy = FieldNamingPolicy.IDENTITY;
ConstructorConstructor constructorConstructor = new ConstructorConstructor(Collections.<Type, InstanceCreator<?>>emptyMap());
MapTypeAdapterFactory defaultMapFactory = new MapTypeAdapterFactory(constructorConstructor, false);
ReflectiveFilterMapFieldFactory defaultObjectFactory = new ReflectiveFilterMapFieldFactory(constructorConstructor,
fieldNamingPolicy, Excluder.DEFAULT);
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> mapAdapter = defaultMapFactory.create(gson, type);
if(mapAdapter!=null){
return (TypeAdapter<T>) new RetainFieldMapAdapter(mapAdapter, defaultObjectFactory.create(gson, type));
}
return mapAdapter;
}
class RetainFieldMapAdapter extends TypeAdapter<Map<String, Object>>{
TypeAdapter<Map<String, Object>> mapAdapter;
ReflectiveTypeAdapterFactory.Adapter<Map<String, Object>> objectAdapter;
RetainFieldMapAdapter(TypeAdapter mapAdapter, ReflectiveTypeAdapterFactory.Adapter objectAdapter) {
this.mapAdapter = mapAdapter;
this.objectAdapter = objectAdapter;
}
#Override
public void write(final JsonWriter out, Map<String, Object> value) throws IOException {
//1.write object
StringWriter sw = new StringWriter();
objectAdapter.write(new JsonWriter(sw), value);
//2.convert object to a map
Map<String, Object> objectMap = mapAdapter.fromJson(sw.toString());
//3.overwrite fields in object to a copy map
value = new LinkedHashMap<String, Object>(value);
value.putAll(objectMap);
//4.write the copy map
mapAdapter.write(out, value);
}
#Override
public Map<String, Object> read(JsonReader in) throws IOException {
//1.create map, all key-value retain in map
Map<String, Object> map = mapAdapter.read(in);
//2.create object from created map
Map<String, Object> object = objectAdapter.fromJsonTree(mapAdapter.toJsonTree(map));
//3.remove fields in object from map
for(String field : objectAdapter.boundFields.keySet()){
map.remove(field);
}
//4.put map to object
object.putAll(map);
return object;
}
}
/**
* If class is extends from some custom map,
* class should implement this to avoid serialize custom map's fields
*/
public interface RetainFieldFlag {}
static class ReflectiveFilterMapFieldFactory extends ReflectiveTypeAdapterFactory{
public ReflectiveFilterMapFieldFactory(ConstructorConstructor constructorConstructor, FieldNamingStrategy fieldNamingPolicy, Excluder excluder) {
super(constructorConstructor, fieldNamingPolicy, excluder);
}
#Override
protected boolean shouldFindFieldInClass(Class willFindClass, Class<?> originalRaw) {
if(RetainFieldFlag.class.isAssignableFrom(originalRaw)){
return RetainFieldFlag.class.isAssignableFrom(willFindClass);
}else{
Class[] endClasses = new Class[]{Object.class, HashMap.class, LinkedHashMap.class,
LinkedTreeMap.class, Hashtable.class, TreeMap.class, ConcurrentHashMap.class,
IdentityHashMap.class, WeakHashMap.class, EnumMap.class};
for(Class c : endClasses){
if(willFindClass == c) return false;
}
}
return super.shouldFindFieldInClass(willFindClass, originalRaw);
}
}
/**
* below code copy from {#link com.google.gson.internal.bind.ReflectiveTypeAdapterFactory}
* (little modify, in source this class is final)
* Type adapter that reflects over the fields and methods of a class.
*/
static class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
private final ConstructorConstructor constructorConstructor;
private final FieldNamingStrategy fieldNamingPolicy;
private final Excluder excluder;
public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
FieldNamingStrategy fieldNamingPolicy, Excluder excluder) {
this.constructorConstructor = constructorConstructor;
this.fieldNamingPolicy = fieldNamingPolicy;
this.excluder = excluder;
}
public boolean excludeField(Field f, boolean serialize) {
return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize);
}
private String getFieldName(Field f) {
SerializedName serializedName = f.getAnnotation(SerializedName.class);
return serializedName == null ? fieldNamingPolicy.translateName(f) : serializedName.value();
}
public <T> Adapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
final Gson context, final Field field, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize) {
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
// special casing primitives here saves ~5% on Android...
return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
final TypeAdapter<?> typeAdapter = context.getAdapter(fieldType);
#SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree
#Override void write(JsonWriter writer, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = field.get(value);
TypeAdapter t = new TypeAdapterRuntimeTypeWrapper(context, this.typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
}
#Override void read(JsonReader reader, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
field.set(value, fieldValue);
}
}
};
}
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw) {
Map<String, BoundField> result = new LinkedHashMap<String, BoundField>();
if (raw.isInterface()) {
return result;
}
Type declaredType = type.getType();
Class<?> originalRaw = type.getRawType();
while (shouldFindFieldInClass(raw, originalRaw)) {
Field[] fields = raw.getDeclaredFields();
for (Field field : fields) {
boolean serialize = excludeField(field, true);
boolean deserialize = excludeField(field, false);
if (!serialize && !deserialize) {
continue;
}
field.setAccessible(true);
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
BoundField boundField = createBoundField(context, field, getFieldName(field),
TypeToken.get(fieldType), serialize, deserialize);
BoundField previous = result.put(boundField.name, boundField);
if (previous != null) {
throw new IllegalArgumentException(declaredType
+ " declares multiple JSON fields named " + previous.name);
}
}
type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
raw = type.getRawType();
}
return result;
}
protected boolean shouldFindFieldInClass(Class willFindClass, Class<?> originalRaw){
return willFindClass != Object.class;
}
static abstract class BoundField {
final String name;
final boolean serialized;
final boolean deserialized;
protected BoundField(String name, boolean serialized, boolean deserialized) {
this.name = name;
this.serialized = serialized;
this.deserialized = deserialized;
}
abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException;
abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException;
}
public static final class Adapter<T> extends TypeAdapter<T> {
private final ObjectConstructor<T> constructor;
private final Map<String, BoundField> boundFields;
private Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
this.constructor = constructor;
this.boundFields = boundFields;
}
#Override public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T instance = constructor.construct();
try {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
}
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
in.endObject();
return instance;
}
#Override public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.beginObject();
try {
for (BoundField boundField : boundFields.values()) {
if (boundField.serialized) {
out.name(boundField.name);
boundField.write(out, value);
}
}
} catch (IllegalAccessException e) {
throw new AssertionError();
}
out.endObject();
}
}
}
static 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 {
// Order of preference for choosing type adapters
// First preference: a type adapter registered for the runtime type
// Second preference: a type adapter registered for the declared type
// Third preference: reflective type adapter for the runtime type (if it is a sub class of the declared type)
// Fourth preference: reflective type adapter for the declared type
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);
}
/**
* Finds a compatible runtime type if it is more specific
*/
private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
if (value != null
&& (type == Object.class || type instanceof TypeVariable<?> || type instanceof Class<?>)) {
type = value.getClass();
}
return type;
}
}
}