A Torre
A Torre

Reputation: 219

Serialize json NaN as null when using GSON

Is it possible to serialize Double NaN as null with GSON?

The issue I currently have is, when I publish Double NaN, a JavaScript subscriber fails to deserialize the json mssage.

class Value {
    Double value;
}

@Test
public void name() {

    Value newValue = new Value();
    newValue.value = Double.NaN;

    Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
    String string = gson.toJson(newValue);

    System.out.println(string);

}

Above code prints:

{"value":NaN}

I would like:

{"value":null}

Upvotes: 0

Views: 2494

Answers (1)

Merely create specialized type adapter factory that overrides Float and Double type adapters and checks serializable values for being NaNs and maybe infinities.

public final class NumberOnlyTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory instance = new NumberOnlyTypeAdapterFactory();

    private static final TypeToken<Float> floatTypeToken = TypeToken.get(Float.class);
    private static final TypeToken<Double> doubleTypeToken = TypeToken.get(Double.class);

    private NumberOnlyTypeAdapterFactory() {
    }

    public static TypeAdapterFactory getInstance() {
        return instance;
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        final Class<? super T> rawType = typeToken.getRawType();
        final TypeAdapter<? extends Number> typeAdapter;
        if ( rawType == Float.class ) {
            typeAdapter = NumberOnlyTypeAdapter.create(gson.getDelegateAdapter(this, floatTypeToken), v -> Float.isNaN(v), v -> Float.isInfinite(v));
        } else if ( rawType == Double.class ) {
            typeAdapter = NumberOnlyTypeAdapter.create(gson.getDelegateAdapter(this, doubleTypeToken), v -> Double.isNaN(v), v -> Double.isInfinite(v));
        } else {
            return null;
        }
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
        return castTypeAdapter;
    }

    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    private static final class NumberOnlyTypeAdapter<T extends Number>
            extends TypeAdapter<T> {

        private final TypeAdapter<? super T> delegate;
        private final Predicate<? super T> isNan;
        private final Predicate<? super T> isInfinite;

        private static <T extends Number> TypeAdapter<T> create(final TypeAdapter<? super T> delegate, final Predicate<? super T> isNan,
                final Predicate<? super T> isInfinite) {
            return new NumberOnlyTypeAdapter<T>(delegate, isNan, isInfinite)
                    .nullSafe();
        }

        @Override
        public void write(final JsonWriter jsonWriter, final T value)
                throws IOException {
            if ( !isNan.test(value) && !isInfinite.test(value) ) {
                delegate.write(jsonWriter, value);
                return;
            }
            jsonWriter.nullValue();
        }

        @Override
        public T read(final JsonReader jsonReader) {
            throw new UnsupportedOperationException("TODO");
        }

    }

}

Here is a test (primitive arrays are used to test primitive floats and doubles whilst the original values are boxed as Float and Double respectively):

public final class NumberOnlyTypeAdapterFactoryTest {

    private static final Gson gson = new GsonBuilder()
            .disableInnerClassSerialization()
            .disableHtmlEscaping()
            .registerTypeAdapterFactory(NumberOnlyTypeAdapterFactory.getInstance())
            .create();

    private static Stream<Arguments> test() {
        return Stream.of(
                Arguments.of(Float.class, 0F, Float.NaN, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, (Function<Float, float[]>) v -> new float[]{ v }),
                Arguments.of(Double.class, 0D, Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, (Function<Double, double[]>) v -> new double[]{ v })
        );
    }

    @ParameterizedTest
    @MethodSource
    public <N extends Number, A> void test(
            final Type type,
            final N zeroValue,
            final N nanValue,
            final N negativeInfinityValue,
            final N positiveInfinityValue,
            final Function<? super N, ? extends A> convertToPrimitiveArray
    ) {
        Assertions.assertEquals("0.0", gson.toJson(zeroValue));
        Assertions.assertEquals("null", gson.toJson(nanValue));
        Assertions.assertEquals("null", gson.toJson(negativeInfinityValue));
        Assertions.assertEquals("null", gson.toJson(positiveInfinityValue));
        Assertions.assertEquals("null", gson.toJson(null, type));
        Assertions.assertEquals("[0.0]", gson.toJson(convertToPrimitiveArray.apply(zeroValue)));
        Assertions.assertEquals("[null]", gson.toJson(convertToPrimitiveArray.apply(nanValue)));
        Assertions.assertEquals("[null]", gson.toJson(convertToPrimitiveArray.apply(negativeInfinityValue)));
        Assertions.assertEquals("[null]", gson.toJson(convertToPrimitiveArray.apply(positiveInfinityValue)));
    }

}

Upvotes: 1

Related Questions