diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java
index b918f123..aef14bea 100644
--- a/gson/src/main/java/com/google/gson/Gson.java
+++ b/gson/src/main/java/com/google/gson/Gson.java
@@ -24,6 +24,7 @@ import com.google.gson.internal.bind.ArrayTypeAdapter;
import com.google.gson.internal.bind.CollectionTypeAdapterFactory;
import com.google.gson.internal.bind.DateTypeAdapter;
import com.google.gson.internal.bind.FieldTypeAdapterFactory;
+import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory;
import com.google.gson.internal.bind.JsonTreeReader;
import com.google.gson.internal.bind.JsonTreeWriter;
import com.google.gson.internal.bind.MapTypeAdapterFactory;
@@ -238,6 +239,7 @@ public final class Gson {
// type adapters for composite and user-defined types
factories.add(new CollectionTypeAdapterFactory(constructorConstructor));
factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization));
+ factories.add(new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor));
factories.add(new FieldTypeAdapterFactory());
factories.add(new ReflectiveTypeAdapterFactory(
constructorConstructor, fieldNamingPolicy, excluder));
diff --git a/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java
new file mode 100644
index 00000000..1a12cfbd
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gson.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.google.gson.TypeAdapter;
+
+/**
+ * An annotation that indicates the Gson {@link TypeAdapter} to use with a class or a field.
+ * Any type adapters registered in {@link com.google.gson.GsonBuilder} supersede the adapter
+ * specified in this annotation.
+ *
+ *
Here is an example of how this annotation is used:
+ *
+ * @JsonAdapter(UserJsonAdapter.class)
+ * public class User {
+ * public final String firstName, lastName;
+ * private User(String firstName, String lastName) {
+ * this.firstName = firstName;
+ * this.lastName = lastName;
+ * }
+ * }
+ * public class UserJsonAdapter extends TypeAdapter<User> {
+ * @Override public void write(JsonWriter out, User user) throws IOException {
+ * // implement write: combine firstName and lastName into name
+ * out.beginObject();
+ * out.name("name");
+ * out.value(user.firstName + " " + user.lastName);
+ * out.endObject();
+ * // implement the write method
+ * }
+ * @Override public User read(JsonReader in) throws IOException {
+ * // implement read: split name into firstName and lastName
+ * in.beginObject();
+ * in.nextName();
+ * String[] nameParts = in.nextString().split(" ");
+ * in.endObject();
+ * return new User(nameParts[0], nameParts[1]);
+ * }
+ * }
+ *
+ *
+ * Since User class specified UserJsonAdapter.class in @JsonAdapter annotation, it
+ * will automatically be invoked to serialize/deserialize User instances.
+ *
+ * If the UserJsonAdapter needs a constructor other than a no-args constructor, you must register
+ * an {@link com.google.gson.InstanceCreator} for it.
+ *
+ * @since 2.3
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @author Jesse Leitch
+ */
+// Note that the above example is taken from JsonAdapterANnotationTest.
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JsonAdapter {
+
+ Class extends TypeAdapter>> value();
+
+}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java
new file mode 100644
index 00000000..0f9d5fb9
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gson.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.ObjectConstructor;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Given a type T, looks for the annotation {@link JsonAdapter} and uses an instance of the
+ * specified class as the default type adapter.
+ *
+ * @since 2.3
+ */
+public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapterFactory {
+
+ private final ConstructorConstructor constructorConstructor;
+
+ public JsonAdapterAnnotationTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
+ this.constructorConstructor = constructorConstructor;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public TypeAdapter create(Gson gson, TypeToken targetType) {
+ Class super T> clazz = targetType.getRawType();
+ JsonAdapter annotation = clazz.getAnnotation(JsonAdapter.class);
+ if (annotation == null) return null;
+ Class extends TypeAdapter>> adapterClass = annotation.value();
+ ObjectConstructor extends TypeAdapter>> constructor = constructorConstructor.get(TypeToken.get(adapterClass));
+ TypeAdapter adapter = constructor.construct();
+ return adapter;
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationTest.java b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationTest.java
new file mode 100644
index 00000000..b84dd4f9
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gson.functional;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import junit.framework.TestCase;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+/**
+ * Functional tests for the {@link com.google.gson.annotations.JsonAdapter} annotation.
+ */
+public final class JsonAdapterAnnotationTest extends TestCase {
+
+ public void testJsonAdapterInvoked() {
+ Gson gson = new Gson();
+ String json = gson.toJson(new A("bar"));
+ assertEquals("\"jsonAdapter\"", json);
+
+ // Also invoke the JsonAdapter javadoc sample
+ json = gson.toJson(new User("Inderjeet", "Singh"));
+ assertEquals("{\"name\":\"Inderjeet Singh\"}", json);
+ User user = gson.fromJson("{'name':'Joel Leitch'}", User.class);
+ assertEquals("Joel", user.firstName);
+ assertEquals("Leitch", user.lastName);
+ }
+
+ public void testRegisteredAdapterOverridesJsonAdapter() {
+ TypeAdapter typeAdapter = new TypeAdapter() {
+ @Override public void write(JsonWriter out, A value) throws IOException {
+ out.value("registeredAdapter");
+ }
+ @Override public A read(JsonReader in) throws IOException {
+ return new A(in.nextString());
+ }
+ };
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(A.class, typeAdapter)
+ .create();
+ String json = gson.toJson(new A("abcd"));
+ assertEquals("\"registeredAdapter\"", json);
+ }
+
+ /**
+ * The serializer overrides field adapter, but for deserializer the fieldAdapter is used.
+ */
+ public void testRegisteredSerializerOverridesJsonAdapter() {
+ JsonSerializer serializer = new JsonSerializer() {
+ public JsonElement serialize(A src, Type typeOfSrc,
+ JsonSerializationContext context) {
+ return new JsonPrimitive("registeredSerializer");
+ }
+ };
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(A.class, serializer)
+ .create();
+ String json = gson.toJson(new A("abcd"));
+ assertEquals("\"registeredSerializer\"", json);
+ A target = gson.fromJson("abcd", A.class);
+ assertEquals("jsonAdapter", target.value);
+ }
+
+ /**
+ * The deserializer overrides Json adapter, but for serializer the jsonAdapter is used.
+ */
+ public void testRegisteredDeserializerOverridesJsonAdapter() {
+ JsonDeserializer deserializer = new JsonDeserializer() {
+ public A deserialize(JsonElement json, Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
+ return new A("registeredDeserializer");
+ }
+ };
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(A.class, deserializer)
+ .create();
+ String json = gson.toJson(new A("abcd"));
+ assertEquals("\"jsonAdapter\"", json);
+ A target = gson.fromJson("abcd", A.class);
+ assertEquals("registeredDeserializer", target.value);
+ }
+
+ public void testIncorrectTypeAdapterFails() {
+ try {
+ String json = new Gson().toJson(new ClassWithIncorrectJsonAdapter("bar"));
+ fail(json);
+ } catch (ClassCastException expected) {}
+ }
+
+ public void testSuperclassTypeAdapterNotInvoked() {
+ String json = new Gson().toJson(new B("bar"));
+ assertFalse(json.contains("jsonAdapter"));
+ }
+
+ @JsonAdapter(A.JsonAdapter.class)
+ private static class A {
+ final String value;
+ A(String value) {
+ this.value = value;
+ }
+ private static final class JsonAdapter extends TypeAdapter {
+ @Override public void write(JsonWriter out, A value) throws IOException {
+ out.value("jsonAdapter");
+ }
+ @Override public A read(JsonReader in) throws IOException {
+ in.nextString();
+ return new A("jsonAdapter");
+ }
+ }
+ }
+
+ private static final class B extends A {
+ B(String value) {
+ super(value);
+ }
+ }
+ // Note that the type is NOT TypeAdapter so this
+ // should cause error
+ @JsonAdapter(A.JsonAdapter.class)
+ private static final class ClassWithIncorrectJsonAdapter {
+ @SuppressWarnings("unused") final String value;
+ ClassWithIncorrectJsonAdapter(String value) {
+ this.value = value;
+ }
+ }
+
+ // This class is used in JsonAdapter Javadoc as an example
+ @JsonAdapter(UserJsonAdapter.class)
+ private static class User {
+ public final String firstName, lastName;
+ private User(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+ }
+ private static class UserJsonAdapter extends TypeAdapter {
+ @Override public void write(JsonWriter out, User user) throws IOException {
+ // implement write: combine firstName and lastName into name
+ out.beginObject();
+ out.name("name");
+ out.value(user.firstName + " " + user.lastName);
+ out.endObject();
+ // implement the write method
+ }
+ @Override public User read(JsonReader in) throws IOException {
+ // implement read: split name into firstName and lastName
+ in.beginObject();
+ in.nextName();
+ String[] nameParts = in.nextString().split(" ");
+ in.endObject();
+ return new User(nameParts[0], nameParts[1]);
+ }
+ }
+
+}