Skip to content

Instantly share code, notes, and snippets.

@JakeWharton
Last active November 28, 2021 12:32
Star You must be signed in to star a gist
Save JakeWharton/0d67d01badcee0ae7bc9 to your computer and use it in GitHub Desktop.
A Gson TypeAdapterFactory which allows serialization of @autovalue types. Apache 2 licensed.
import com.google.auto.value.AutoValue;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Marks an {@link AutoValue @AutoValue}-annotated type for proper Gson serialization.
* <p>
* This annotation is needed because the {@linkplain Retention retention} of {@code @AutoValue}
* does not allow reflection at runtime.
*/
@Target(TYPE)
@Retention(RUNTIME)
public @interface AutoGson {
}
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
public final class AutoValueAdapterFactory implements TypeAdapterFactory {
@SuppressWarnings("unchecked")
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<? super T> rawType = type.getRawType();
if (!rawType.isAnnotationPresent(AutoGson.class)) {
return null;
}
String packageName = rawType.getPackage().getName();
String className = rawType.getName().substring(packageName.length() + 1).replace('$', '_');
String autoValueName = packageName + ".AutoValue_" + className;
try {
Class<?> autoValueType = Class.forName(autoValueName);
return (TypeAdapter<T>) gson.getAdapter(autoValueType);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Could not load AutoValue type " + autoValueName, e);
}
}
}
import com.google.auto.value.AutoValue;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class Main {
public static void main(String... args) {
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new AutoValueAdapterFactory())
.create();
Test inTest = Test.of("John", "Doe", 100);
System.out.println("IN: " + inTest);
String json = gson.toJson(inTest);
System.out.println("JSON: " + json);
Test outTest = gson.fromJson(json, Test.class);
System.out.println("OUT: " + outTest);
}
@AutoValue @AutoGson
public abstract static class Test {
public static Test of(String firstName, String lastName, int age) {
return new AutoValue_Main_Test(firstName, lastName, age);
}
public abstract String firstName();
public abstract String lastName();
public abstract int age();
}
}
@thenuge
Copy link

thenuge commented Nov 9, 2014

Very useful, thanks! I made a slight adjustment to make it work with obfuscation, since using hardcoded strings to find the AutoValue-generated class no longer works once you start obfuscating class names. It just takes a class reference instead:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AutoGson {
  // A reference to the AutoValue-generated class (e.g. AutoValue_MyClass). This is
  // necessary to handle obfuscation of the class names.
  public Class autoValueClass();
}
public class AutoValueTypeAdapterFactory implements TypeAdapterFactory {
  @Override
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    Class<T> rawType = (Class<T>) type.getRawType();

    AutoGson annotation = rawType.getAnnotation(AutoGson.class);
    // Only deserialize classes decorated with @AutoGson.
    if (annotation == null) {
      return null;
    }

    return (TypeAdapter<T>) gson.getAdapter(annotation.autoValueClass());
  }
}

Then, you just need to decorate your AutoValue class like so (following your example):

  @AutoValue 
  @AutoGson(autoValueClass = AutoValue_Main_Test.class)
  public abstract static class Test {
    public static Test of(String firstName, String lastName, int age) {
      return new AutoValue_Main_Test(firstName, lastName, age);
    }

    public abstract String firstName();
    public abstract String lastName();
    public abstract int age();
  }

@jacek-marchwicki
Copy link

If someone need some simple test for @thenuge solution

import com.google.auto.value.AutoValue;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import org.junit.Before;
import org.junit.Test;

import static com.google.common.truth.Truth.assert_;

public class AutoValueAdapterFactoryTest {

    private Gson mGson;

    @AutoValue
    @AutoGson(autoValueClass = AutoValue_AutoValueAdapterFactoryTest_Some.class)
    public abstract static class Some {
        public static Some of(String firstName, String lastName, int age) {
            return new AutoValue_AutoValueAdapterFactoryTest_Some(firstName, lastName, age);
        }

        public abstract String firstName();
        public abstract String lastName();
        public abstract int age();
    }

    @Before
    public void setUp() throws Exception {
        mGson = new GsonBuilder()
                .registerTypeAdapterFactory(new AutoValueAdapterFactory())
                .create();
    }

    @Test
    public void testParsing() throws Exception {
        final Some inSome = Some.of("John", "Doe", 100);
        final String json = mGson.toJson(inSome);
        final Some outSome = mGson.fromJson(json, Some.class);

        assert_().that(outSome).isEqualTo(inSome);
    }

}

@codedance
Copy link

Rather than using a new Annotation, testing for an abstract class may be a little more automatic.

public final class AutoValueAdapterFactory implements TypeAdapterFactory {
  @SuppressWarnings("unchecked")
  @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    Class<? super T> rawType = type.getRawType();

    // If it's an abstract class, it's likely to be an AutoValue
    int m = rawType.getModifiers();
    if (!Modifier.isAbstract(m)) {
        return null;
    }

    String packageName = rawType.getPackage().getName();
    String className = rawType.getName().substring(packageName.length() + 1).replace('$', '_');
    String autoValueName = packageName + ".AutoValue_" + className;

    try {
      Class<?> autoValueType = Class.forName(autoValueName);
      return (TypeAdapter<T>) gson.getAdapter(autoValueType);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException("Could not load AutoValue type " + autoValueName, e);
    }
  }
}

@pedro-ribeiro
Copy link

Hi!

I'm trying to test this chunk of code and I'm always bumping into a no-args contructor problem.

Looking at the code generated by AutoValue, there is indeed no no-args constructor (as it should, afaik).

What am I missing?

Thanks!

@JakeWharton
Copy link
Author

@codedance That is far too presumptuous to work at scale.

@semanticer
Copy link

Is there a way how to use @SerializedName with AutoValue and this TypeAdapterFactory?

@efung
Copy link

efung commented May 19, 2016

@oldergod
Copy link

oldergod commented Sep 22, 2016

Also interested in adding SerializedName.

@lukasz-gosiewski
Copy link

lukasz-gosiewski commented Dec 5, 2016

Ok, what about Builders ? AutoValue for android support builder pattern, however when i use it with your @AutoGson it's giving me objects full of nulls. Is there any good way to go or i need to write my own solution ?

@danielesegato
Copy link

danielesegato commented Feb 3, 2017

Does this work with generics?

@AutoValue @AutoGson
public abstract static class Some<T> {
  public static Some <X> of(String firstName, String lastName, int age, X genericData) {
    return new AutoValue_Main_Test<>(firstName, lastName, age, genericData);
  }

  public abstract String firstName();
  public abstract String lastName();
  public abstract int age();
  public abstract T genericData();
}

When you do something like:

public class People {
  List<Some<Map<String,String>> someList;
}

And try to parse a "People" json, will the generic be resolved?

@Ajibola
Copy link

Ajibola commented Dec 12, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment