Performance Tuning Gson: Best Practices and Benchmarks

How to Customize Serialization and Deserialization with GsonGson is a lightweight Java library from Google for converting Java objects to JSON and back. While Gson’s default behavior covers many use cases, real-world applications often require customization: renaming fields, excluding properties, handling polymorphism, formatting dates, or implementing custom logic for specific types. This article walks through practical techniques for customizing serialization and deserialization with Gson, with clear examples and recommended approaches.


Table of contents

  • Why customize Gson?
  • Basic Gson usage recap
  • Field naming strategies
  • Excluding fields and conditional serialization
  • Custom serializers and deserializers (TypeAdapter and JsonSerializer/JsonDeserializer)
  • Handling polymorphism (subtypes)
  • Working with dates and times
  • Adapters for collections and maps
  • Runtime type adapters and delegation
  • Best practices and performance tips
  • Complete examples

Why customize Gson?

Gson’s defaults are convenient: it maps fields to JSON keys by name, supports primitives and common collections, and handles nested objects automatically. But you’ll want custom behavior when:

  • Your JSON field names don’t match Java field names.
  • You need to ignore sensitive fields (passwords, tokens) or transient data.
  • You must enforce special formatting (dates, numbers).
  • You require polymorphic deserialization (interface/abstract type to concrete subclasses).
  • You need to apply validation, default values, or transformation logic during (de)serialization.

Basic Gson usage recap

A quick refresh: serialize and deserialize with the default Gson instance.

Gson gson = new Gson(); String json = gson.toJson(myObject); MyClass obj = gson.fromJson(json, MyClass.class); 

For more control, use GsonBuilder:

Gson gson = new GsonBuilder()     .setPrettyPrinting()     .create(); 

Field naming strategies

If JSON uses different field names (e.g., snake_case) than your Java fields (camelCase), use:

  • @SerializedName — explicit per-field mapping.
  • FieldNamingStrategy or built-in policies like LOWER_CASE_WITH_UNDERSCORES.

Example with @SerializedName:

class User {     @SerializedName("user_id")     private int userId;     @SerializedName("full_name")     private String name; } 

Using built-in naming policy:

Gson gson = new GsonBuilder()     .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)     .create(); 

Excluding fields and conditional serialization

To exclude fields:

  • transient keyword excludes fields by default.
  • @Expose with GsonBuilder().excludeFieldsWithoutExposeAnnotation().
  • custom ExclusionStrategy for logic-based exclusion.

Using @Expose:

class Secret {     @Expose     private String publicData;     @Expose(serialize = false, deserialize = false)     private String hidden; } Gson gson = new GsonBuilder()     .excludeFieldsWithoutExposeAnnotation()     .create(); 

Custom ExclusionStrategy:

public class SensitiveExclusionStrategy implements ExclusionStrategy {     public boolean shouldSkipField(FieldAttributes f) {         return f.getName().equals("password");     }     public boolean shouldSkipClass(Class<?> clazz) {         return false;     } } Gson gson = new GsonBuilder()     .setExclusionStrategies(new SensitiveExclusionStrategy())     .create(); 

Custom serializers and deserializers

For types needing non-default handling, implement:

  • JsonSerializer and JsonDeserializer
  • TypeAdapter for full control (including streaming)

JsonSerializer/JsonDeserializer example:

class Money {     private BigDecimal amount;     private String currency;     // getters/setters } class MoneySerializer implements JsonSerializer<Money> {     @Override     public JsonElement serialize(Money src, Type typeOfSrc, JsonSerializationContext context) {         JsonObject obj = new JsonObject();         obj.addProperty("amount", src.getAmount().toPlainString());         obj.addProperty("currency", src.getCurrency());         return obj;     } } class MoneyDeserializer implements JsonDeserializer<Money> {     @Override     public Money deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)         throws JsonParseException {         JsonObject obj = json.getAsJsonObject();         BigDecimal amount = new BigDecimal(obj.get("amount").getAsString());         String currency = obj.get("currency").getAsString();         Money m = new Money();         m.setAmount(amount);         m.setCurrency(currency);         return m;     } } Gson gson = new GsonBuilder()     .registerTypeAdapter(Money.class, new MoneySerializer())     .registerTypeAdapter(Money.class, new MoneyDeserializer())     .create(); 

TypeAdapter example (streaming, faster, more control):

public class MoneyTypeAdapter extends TypeAdapter<Money> {     @Override     public void write(JsonWriter out, Money value) throws IOException {         out.beginObject();         out.name("amount").value(value.getAmount().toPlainString());         out.name("currency").value(value.getCurrency());         out.endObject();     }     @Override     public Money read(JsonReader in) throws IOException {         Money m = new Money();         in.beginObject();         while (in.hasNext()) {             String name = in.nextName();             if ("amount".equals(name)) m.setAmount(new BigDecimal(in.nextString()));             else if ("currency".equals(name)) m.setCurrency(in.nextString());             else in.skipValue();         }         in.endObject();         return m;     } } Gson gson = new GsonBuilder()     .registerTypeAdapter(Money.class, new MoneyTypeAdapter())     .create(); 

Handling polymorphism (subtypes)

Gson doesn’t handle polymorphism automatically. Approaches:

  • Add a type discriminator in JSON and use a custom TypeAdapter or JsonDeserializer to switch on it.
  • Use RuntimeTypeAdapterFactory (community extension) for registration-based handling.
  • Manually inspect JSON in a custom deserializer.

Example using a simple discriminator:

abstract class Animal { String name; } class Dog extends Animal { int barkVolume; } class Cat extends Animal { boolean likesCream; } class AnimalDeserializer implements JsonDeserializer<Animal> {     @Override     public Animal deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {         JsonObject obj = json.getAsJsonObject();         String type = obj.get("type").getAsString();         switch (type) {             case "dog": return context.deserialize(obj, Dog.class);             case "cat": return context.deserialize(obj, Cat.class);             default: throw new JsonParseException("Unknown type: " + type);         }     } } Gson gson = new GsonBuilder()     .registerTypeAdapter(Animal.class, new AnimalDeserializer())     .create(); 

Working with dates and times

Gson has limited built-in support for java.util.Date and java.sql.*. For java.time (Java 8+), write adapters or use third-party modules.

Built-in date formatting:

Gson gson = new GsonBuilder()     .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")     .create(); 

Java Time example using serializers:

GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(LocalDate.class, new JsonSerializer<LocalDate>() {     public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) {         return new JsonPrimitive(src.toString()); // ISO-8601     } }); builder.registerTypeAdapter(LocalDate.class, new JsonDeserializer<LocalDate>() {     public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {         return LocalDate.parse(json.getAsString());     } }); Gson gson = builder.create(); 

Adapters for collections and maps

Gson handles most collections automatically, but sometimes you need custom handling:

  • Convert sets to arrays and back.
  • Map keys that are objects (not strings) — write a custom adapter that serializes map entries as objects with key/value fields.
  • Preserve element order for LinkedHashMap/LinkedHashSet by using TypeToken to register specific collection types if necessary.

Example: Map with non-string keys:

class ComplexKey {     int id;     String label;     // equals/hashCode } class ComplexKeyMapAdapter implements JsonSerializer<Map<ComplexKey, String>>, JsonDeserializer<Map<ComplexKey, String>> {     @Override     public JsonElement serialize(Map<ComplexKey, String> src, Type typeOfSrc, JsonSerializationContext context) {         JsonArray arr = new JsonArray();         for (Map.Entry<ComplexKey, String> e : src.entrySet()) {             JsonObject obj = new JsonObject();             obj.add("key", context.serialize(e.getKey()));             obj.addProperty("value", e.getValue());             arr.add(obj);         }         return arr;     }     @Override     public Map<ComplexKey, String> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {         Map<ComplexKey, String> map = new LinkedHashMap<>();         JsonArray arr = json.getAsJsonArray();         for (JsonElement el : arr) {             JsonObject obj = el.getAsJsonObject();             ComplexKey key = context.deserialize(obj.get("key"), ComplexKey.class);             String value = obj.get("value").getAsString();             map.put(key, value);         }         return map;     } } 

Runtime type adapters and delegation

Sometimes you want to decorate behavior: wrap another adapter or delegate to Gson’s default adapter for parts of the work. Use Gson.getDelegateAdapter to obtain the default and extend it.

Example: adding validation after default deserialization:

class ValidatingAdapterFactory implements TypeAdapterFactory {     public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {         final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);         return new TypeAdapter<T>() {             @Override             public void write(JsonWriter out, T value) throws IOException {                 delegate.write(out, value);             }             @Override             public T read(JsonReader in) throws IOException {                 T obj = delegate.read(in);                 // perform validation, throw JsonParseException on failure                 return obj;             }         };     } } Gson gson = new GsonBuilder()     .registerTypeAdapterFactory(new ValidatingAdapterFactory())     .create(); 

Best practices and performance tips

  • Prefer TypeAdapter for performance-critical paths; it uses streaming APIs and avoids intermediate trees.
  • Reuse Gson instances — they are thread-safe and expensive to build.
  • Keep custom adapters small and focused; composition is better than huge monolithic adapters.
  • Use delegate adapters to combine default behavior with custom logic.
  • For large JSON, consider streaming parsing (JsonReader) to reduce memory.
  • Avoid reflection-heavy logic in hot paths; cache metadata if needed.

Complete example: Putting it together

A compact example showing naming, exclusion, date handling, and a custom adapter:

class User {     @SerializedName("id")     private String id;     @SerializedName("name")     private String name;     @Expose(serialize = false)     private String password;     private LocalDate registered;     // getters/setters } // LocalDate adapter class LocalDateAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {     public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) {         return new JsonPrimitive(src.toString());     }     public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {         return LocalDate.parse(json.getAsString());     } } Gson gson = new GsonBuilder()     .excludeFieldsWithoutExposeAnnotation()     .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())     .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)     .setPrettyPrinting()     .create(); 

When to avoid heavy customization

If your JSON schema is stable and maps closely to your Java model, heavy customization adds maintenance overhead. Use DTOs to map incoming/outgoing JSON and keep domain objects clean. For complex mappings, consider model transformation steps (DTO -> domain) rather than embedding all logic in Gson adapters.


Summary

Customizing Gson allows precise control over JSON representations: field naming, exclusion policies, custom formats, polymorphism, and high-performance streaming. Use @SerializedName, @Expose, custom serializers/deserializers, TypeAdapter, and TypeAdapterFactory as appropriate. Prefer reuse of Gson instances, keep adapters focused, and favor TypeAdapter for performance-critical code.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *