diff --git a/gson/src/org/immutables/gson/Gson.java b/gson/src/org/immutables/gson/Gson.java index 5b96f70ff..9e36fce7b 100644 --- a/gson/src/org/immutables/gson/Gson.java +++ b/gson/src/org/immutables/gson/Gson.java @@ -18,6 +18,7 @@ import com.google.gson.FieldNamingPolicy; import com.google.gson.FieldNamingStrategy; import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -175,4 +176,12 @@ @Documented @Target(ElementType.METHOD) public @interface Ignore {} + + /** + * Can be put on accessor of type {@link JsonObject} as catch all collection of all other fields, + * not mapped to declared attributes (i.e. unknown). + */ + @Documented + @Target(ElementType.METHOD) + public @interface Other {} } diff --git a/gson/test/org/immutables/gson/adapter/GsonFeaturesTest.java b/gson/test/org/immutables/gson/adapter/GsonFeaturesTest.java index 842aa0a29..aa60f6fb1 100644 --- a/gson/test/org/immutables/gson/adapter/GsonFeaturesTest.java +++ b/gson/test/org/immutables/gson/adapter/GsonFeaturesTest.java @@ -20,7 +20,9 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import java.util.Map; import java.util.Set; import org.junit.Test; @@ -32,6 +34,7 @@ public class GsonFeaturesTest { .serializeNulls() .registerTypeAdapterFactory(new GsonAdaptersSimple()) .registerTypeAdapterFactory(new GsonAdaptersUnsimple()) + .registerTypeAdapterFactory(new GsonAdaptersOtherAttributes()) .create(); final Gson gsonDefault = new GsonBuilder() @@ -123,6 +126,17 @@ public void nullableContainersNames() { check(json).contains("\"m\":"); } + @Test + public void otherAttributes() { + String json = "{\"a\":1,\"b\":\"B\",\"c\":true,\"d\":null}"; + OtherAttributes o = gsonWithOptions.fromJson(json, OtherAttributes.class); + + check(o.rest().get("c")).is(new JsonPrimitive(true)); + check(o.rest().get("d")).is(JsonNull.INSTANCE); + + check(gsonWithOptions.toJson(o)).is(json); + } + private Set keysIn(JsonObject json) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Map.Entry entry : json.entrySet()) { diff --git a/gson/test/org/immutables/gson/adapter/OtherAttributes.java b/gson/test/org/immutables/gson/adapter/OtherAttributes.java new file mode 100644 index 000000000..4cea437c1 --- /dev/null +++ b/gson/test/org/immutables/gson/adapter/OtherAttributes.java @@ -0,0 +1,16 @@ +package org.immutables.gson.adapter; + +import com.google.gson.JsonObject; +import org.immutables.gson.Gson; +import org.immutables.value.Value; + +@Gson.TypeAdapters +@Value.Immutable +public interface OtherAttributes { + int a(); + + String b(); + + @Gson.Other + JsonObject rest(); +} diff --git a/value-processor/src/org/immutables/value/processor/Gsons.generator b/value-processor/src/org/immutables/value/processor/Gsons.generator index 20f86b010..afad9d434 100644 --- a/value-processor/src/org/immutables/value/processor/Gsons.generator +++ b/value-processor/src/org/immutables/value/processor/Gsons.generator @@ -141,6 +141,9 @@ private static class [type.name]TypeAdapter[type.generics] extends TypeAdapter<[ public final [a.secondaryElementType] [a.name]SecondaryTypeSample = null; [/if] [/for] + [for other = type.gsonOther][if other] + private final TypeAdapter [other.name]TypeAdapter; + [/if][/for] [for a in allAttributes] [if a.requiresMarshalingAdapter and a.primitiveArrayType] private final TypeAdapter<[a.type]> [a.name]TypeAdapter; @@ -171,6 +174,9 @@ private static class [type.name]TypeAdapter[type.generics] extends TypeAdapter<[ [if type.generics] java.lang.reflect.Type['[]'] typeArguments = getTypeArguments(type); [/if] + [for other = type.gsonOther][if other] + this.[other.name]TypeAdapter = gson.getAdapter(JsonElement.class); + [/if][/for] [for a in allAttributes] [if a.requiresMarshalingAdapter] [getAdapterFromGson a false] @@ -259,11 +265,21 @@ private void write[type.name](JsonWriter out, [type.typeAbstract] instance) [for a in type.marshaledAttributes] [generateMarshalAttributeValue type a] [/for] + [generateMarshalOther type] out.endObject(); [/if] } [/template] +[template generateMarshalOther Type type] +[for other = type.gsonOther][if other] +for (java.util.Map.Entry other : instance.[other.names.get]().entrySet()) { + out.name(other.getKey()); + [other.name]TypeAdapter.write(out, other.getValue()); +} +[/if][/for] +[/template] + [template generateMarshalConstructorValue Type type Attribute a] [if a.optionalType] [a.type] [a.name]Optional = instance.[a.names.get](); @@ -514,11 +530,19 @@ private [if type.generics.empty][if not type.unmarshaledAttributes]static[/if][/ } [type.typeBuilder] builder = [castBuildStagedBuilder type][type.factoryBuilder]()[/castBuildStagedBuilder]; [if type.unmarshaledAttributes] + [for other = type.gsonOther] + [if other] + JsonObject other = new JsonObject(); + [/if] in.beginObject(); while (in.hasNext()) { - eachAttribute(in, builder); + eachAttribute(in, builder[if other], other[/if]); } in.endObject(); + [if other] + builder.[other.names.init](other); + [/if] + [/for] [else] in.skipValue(); [/if] @@ -526,7 +550,7 @@ private [if type.generics.empty][if not type.unmarshaledAttributes]static[/if][/ } [if type.unmarshaledAttributes] -private void eachAttribute(JsonReader in, [type.typeBuilder] builder) +private void eachAttribute(JsonReader in, [type.typeBuilder] builder[if type.gsonOther], JsonObject other[/if]) throws IOException { String attributeName = in.nextName(); [if type.gsonTypeAdapters.fieldNamingStrategy] @@ -558,7 +582,13 @@ private void eachAttribute(JsonReader in, [type.typeBuilder] builder) default: } [/if] + [for other = type.gsonOther] + [if other] + other.add(attributeName, [other.name]TypeAdapter.read(in)); + [else] in.skipValue(); + [/if] + [/for] } [/if] [for s in type.unmarshaledAttributes] diff --git a/value-processor/src/org/immutables/value/processor/meta/GsonMirrors.java b/value-processor/src/org/immutables/value/processor/meta/GsonMirrors.java index b63d5b263..874c4e5ac 100644 --- a/value-processor/src/org/immutables/value/processor/meta/GsonMirrors.java +++ b/value-processor/src/org/immutables/value/processor/meta/GsonMirrors.java @@ -20,6 +20,8 @@ public final class GsonMirrors { private GsonMirrors() {} + public static String JSON_OBJECT_TYPE = "com.google.gson.JsonObject"; + @Mirror.Annotation("org.immutables.gson.Gson.TypeAdapters") public @interface TypeAdapters { boolean metainfService() default true; @@ -50,4 +52,7 @@ private GsonMirrors() {} @Mirror.Annotation("org.immutables.gson.Gson.Ignore") public @interface Ignore {} + + @Mirror.Annotation("org.immutables.gson.Gson.Other") + public @interface GsonOther {} } diff --git a/value-processor/src/org/immutables/value/processor/meta/ValueAttribute.java b/value-processor/src/org/immutables/value/processor/meta/ValueAttribute.java index 6abc2ca9b..e061a57de 100644 --- a/value-processor/src/org/immutables/value/processor/meta/ValueAttribute.java +++ b/value-processor/src/org/immutables/value/processor/meta/ValueAttribute.java @@ -289,6 +289,26 @@ public String getMarshaledName() { return names.raw; } + private @Nullable Boolean isGsonOther = null; + + public boolean isGsonOther() { + if (isGsonOther == null) { + if (GsonOtherMirror.isPresent(element)) { + if (!isGenerateAbstract || !rawTypeName.equals(GsonMirrors.JSON_OBJECT_TYPE)) { + report().error( + "@Gson.Other attribute must be abstract accessor of type %s", + GsonMirrors.JSON_OBJECT_TYPE); + isGsonOther = false; + } else { + isGsonOther = true; + } + } else { + isGsonOther = false; + } + } + return isGsonOther; + } + public boolean isForcedEmpty() { return !containingType.gsonTypeAdapters().emptyAsNulls(); } diff --git a/value-processor/src/org/immutables/value/processor/meta/ValueType.java b/value-processor/src/org/immutables/value/processor/meta/ValueType.java index e1540ec8e..e516e649d 100644 --- a/value-processor/src/org/immutables/value/processor/meta/ValueType.java +++ b/value-processor/src/org/immutables/value/processor/meta/ValueType.java @@ -877,9 +877,9 @@ private FluentIterable attributes() { public List getMarshaledAttributes() { ImmutableList.Builder builder = ImmutableList.builder(); - for (ValueAttribute attribute : getImplementedAttributes()) { - if (!attribute.isJsonIgnore()) { - builder.add(attribute); + for (ValueAttribute a : getImplementedAttributes()) { + if (!a.isJsonIgnore() && !a.isGsonOther()) { + builder.add(a); } } return builder.build(); @@ -887,9 +887,9 @@ public List getMarshaledAttributes() { public List getUnmarshaledAttributes() { ImmutableList.Builder builder = ImmutableList.builder(); - for (ValueAttribute attribute : getSettableAttributes()) { - if (!attribute.isJsonIgnore()) { - builder.add(attribute); + for (ValueAttribute a : getSettableAttributes()) { + if (!a.isJsonIgnore() && !a.isGsonOther()) { + builder.add(a); } } return builder.build(); @@ -1697,6 +1697,15 @@ public GsonTypeTokens getGsonTypeTokens() { } return gsonTypeTokens; } + + public @Nullable ValueAttribute getGsonOther() { + for (ValueAttribute a : attributes) { + if (a.isGsonOther()) { + return a; + } + } + return null; + } private @Nullable TypeExtractor typeExtractor;