Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RuntimeTypeAdapterFactory overrides explicit control over serializeNulls behavior of contained objects #676

Open
lcoote opened this issue Aug 4, 2015 · 0 comments · May be fixed by #2151
Labels

Comments

@lcoote
Copy link
Contributor

lcoote commented Aug 4, 2015

By default serializeNulls behavior is off for GSON which is useful for building compact JSON. However, for specific types of objects its necessary to switch this on to make a distinction between (a) properties for which no explicit value has been assigned vs (b) properties for which a null value has been explicitly assigned.

One way this is achieved is to use a TypeAdapter implementation to temporarily override the setting of this behavior on the JsonWriter.

E.g.

static class TypeAdapterForSomeObject extends TypeAdapter<SomeObject> {

    @Override
    public void write(JsonWriter out, SomeObject value) throws IOException {
        boolean oldSerializeNulls = out.getSerializeNulls();
        try {
            out.setSerializeNulls(true);
            // serialization impl...
        } finally {
            out.setSerializeNulls(oldSerializeNulls);
        }
    }

    // ...

}

However, when such a type is part of another class which is serialized within another wrapper object which relies on a RuntimeTypeAdapterFactory, these explicit null override behavior can be lost. That is because, the implementation of TypeAdapter.write initially serializes the outer value using a temporary JsonTreeWriter into a JsonElement. This temporary writer invokes the custom TypeAdapter and thus accurately collects the explicit null values. However, RuntimeTypeAdapterFactory then serializes the JsonElement using the externally provided JsonWriter using its own serializeNulls configuration and therefore the explicit null values in the element are discarded.

A full test is included below:

public class Tests {

     // define value requiring explicit nulls
    static class Inner {
        String prop;
        Inner(String prop) {
            this.prop = prop;
        }
    }

    // define adapter to switch on explicit null
    static class TypeAdapterForInner extends TypeAdapter<Inner> {

        @Override
        public void write(JsonWriter out, Inner value) throws IOException {
            boolean oldSerializeNulls = out.getSerializeNulls();
            try {
                out.setSerializeNulls(true);
                out.beginObject().name("prop").value(value.prop).endObject();
            } finally {
                out.setSerializeNulls(oldSerializeNulls);
            }
        }

        @Override
        public Inner read(JsonReader in) throws IOException {
            in.beginObject();
            in.nextName();
            String value = null;
            if (in.peek() == JsonToken.STRING) {
                value = in.nextString();
            } else {
                in.nextNull();
            }
            in.endObject();
            return new Inner(value);
        }

    }

    // define wrapper type hierarchy for use  by RuntimeTypeAdapterFactory
    static abstract class Outer {
        Inner wrapped;
        Outer(Inner wrapped) {
            this.wrapped = wrapped;
        }
    }

    static class OuterA extends Outer {
        OuterA(Inner wrapped) {
            super(wrapped);
        }
    }

    static class OuterB extends Outer {
        OuterB(Inner wrapped) {
            super(wrapped);
        }
    }

    @Test
    public void testSerializingToPreserveNullsInEmbeddedObjects() {
        TypeAdapterFactory metaWrapperAdapter = RuntimeTypeAdapterFactory.of(Outer.class, "kind")
                .registerSubtype(OuterA.class, "a")
                .registerSubtype(OuterB.class, "b");

        Gson gson = new GsonBuilder()
                .registerTypeAdapterFactory(metaWrapperAdapter)
                .registerTypeAdapter(Inner.class, new TypeAdapterForInner().nullSafe())
                .create();

        // verify, null serialization works for unwrapped, inner value
        Inner inner = new Inner(null);        
        assertJsonEquivalent(
                "{ prop: null }",
                gson.toJson(inner));

        OuterB outer = new OuterB(inner); 
        // will fail; actual = "{ type: 'b', wrapped: {} }"
        assertJsonEquivalent(
                "{ type: 'b', wrapped: { prop: null } }",
                gson.toJson(outer, Outer.class));
    }

    private static void assertJsonEquivalent(String expected, String actual) {
        JsonElement expectedElem = new JsonParser().parse(expected);
        JsonElement actualElem = new JsonParser().parse(actual);
        assertEquals(expectedElem, actualElem);
    }

}
lcoote pushed a commit to lcoote/gson that referenced this issue Aug 4, 2015
Using custom TypeAdapters, it is possible to force null values within a
particular type to be explicitly serialized, by overriding the
setSerializeNulls setting on the supplied JsonWriter.

However, this behavior fails in the context of a
RuntimeTypeAdapterFactory because a second JsonTreeWriter is used and
settings are not propagated from one to the other.

This change fixes this by:

- propagating the original setSerializeNulls setting to the created
JsonTreeWriter
- temporarily forcing the setSerializeNulls setting to true on the
oriiginal JsonWriter to ensure any explicit nulls are preserved.

Issue google#676
lcoote added a commit to lcoote/gson that referenced this issue Aug 5, 2015
Using custom TypeAdapters, it is possible to force null values within a
particular type to be explicitly serialized, by overriding the
setSerializeNulls setting on the supplied JsonWriter.

However, this behavior fails in the context of a
RuntimeTypeAdapterFactory because a second JsonTreeWriter is used and
settings are not propagated from one to the other.

This change fixes this by:

- propagating the original setSerializeNulls setting to the created
JsonTreeWriter
- temporarily forcing the setSerializeNulls setting to true on the
oriiginal JsonWriter to ensure any explicit nulls are preserved.

Issue google#676
@Marcono1234 Marcono1234 added the bug label Aug 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants