Course – LS (cat=JSON/Jackson)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this short tutorial, we’re going to explore the available options for excluding one or more fields of a Java class and its subclasses from Gson serialization.

2. Initial Setup

Let’s first define our classes:

@Data
@AllArgsConstructor
public class MyClass {
    private long id;
    private String name;
    private String other;
    private MySubClass subclass;
}

@Data
@AllArgsConstructor
public class MySubClass {
    private long id;
    private String description;
    private String otherVerboseInfo;
}

We’ve annotated them with Lombok for convenience (syntactic sugar for getters, setters, constructors…).

Let’s now populate them:

MySubClass subclass = new MySubClass(42L, "the answer", "Verbose field not to serialize")
MyClass source = new MyClass(1L, "foo", "bar", subclass);

Our goal is to prevent the MyClass.other and MySubClass.otherVerboseInfo fields from being serialized.

The output we are expecting to get is:

{
  "id":1,
  "name":"foo",
  "subclass":{
    "id":42,
    "description":"the answer"
  }
}

In Java:

String expectedResult = "{\"id\":1,\"name\":\"foo\",\"subclass\":{\"id\":42,\"description\":\"the answer\"}}";

3. Transient Modifier

We can mark a field with the transient modifier:

public class MyClass {
    private long id;
    private String name;
    private transient String other;
    private MySubClass subclass;
}

public class MySubClass {
    private long id;
    private String description;
    private transient String otherVerboseInfo;
}

Gson serializer will ignore every field declared as transient:

String jsonString = new Gson().toJson(source);
assertEquals(expectedResult, jsonString);

While this is very fast, it also comes with a severe downside: every serialization tool will take transient into account, not only Gson.

Transient is the Java way to exclude from serialization, then our field will also be filtered out by Serializable‘s serialization, and by every library tool or framework managing our objects.

Additionally, the transient keyword always works for both serialization and deserialization, which can be limiting depending on the use-cases.

4. @Expose Annotation

Gson com.google.gson.annotations @Expose annotation works the other way around.

We can use it to declare which fields to serialize, and ignore the others:

public class MyClass {
    @Expose 
    private long id;
    @Expose 
    private String name;
    private String other;
    @Expose 
    private MySubClass subclass;
}

public class MySubClass {
    @Expose 
    private long id;
    @Expose 
    private String description;
    private String otherVerboseInfo;
}   

For this, we need to instantiate Gson with a GsonBuilder:

Gson gson = new GsonBuilder()
  .excludeFieldsWithoutExposeAnnotation()
  .create();
String jsonString = gson.toJson(source);
assertEquals(expectedResult, jsonString);

This time we can control at field level whether the filtering should happen for serialization, deserialization, or both (default).

Let’s see how to prevent MyClass.other from being serialized, but allowing it to be populated during a deserialization from JSON:

@Expose(serialize = false, deserialize = true) 
private String other;

While this is the easiest way provided by Gson, and it doesn’t affect other libraries, it could imply redundancy in the code. If we have a class with a hundred of fields, and we only want to exclude one field, we need to write ninety-nine annotation, which is overkill.

5. ExclusionStrategy

A highly customizable solution is the usage of a com.google.gson.ExclusionStrategy.

It allows us to define (externally or with an Anonimous Inner Class) a strategy to instruct the GsonBuilder whether to serialize fields (and/or classes) with custom criteria.

Gson gson = new GsonBuilder()
  .addSerializationExclusionStrategy(strategy)
  .create();
String jsonString = gson.toJson(source);

assertEquals(expectedResult, jsonString);

Let’s see some examples of smart strategies to use.

5.1. With Classes and Fields Names

Of course, we can also hardcode one or more fields/classes names:

ExclusionStrategy strategy = new ExclusionStrategy() {
    @Override
    public boolean shouldSkipField(FieldAttributes field) {
        if (field.getDeclaringClass() == MyClass.class && field.getName().equals("other")) {
            return true;
        }
        if (field.getDeclaringClass() == MySubClass.class && field.getName().equals("otherVerboseInfo")) {
            return true;
        }
        return false;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
};

This is fast and straight to the point, but not very reusable and also prone to errors in case we rename our attributes.

5.2. With Business Criteria

Since we simply have to return a boolean, we can implement every business logic we like inside that method.

In the following example, we’ll identify every field starting with “other” as fields which shouldn’t be serialized, no matter the class they belong:

ExclusionStrategy strategy = new ExclusionStrategy() {
    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }

    @Override
    public boolean shouldSkipField(FieldAttributes field) {
        return field.getName().startsWith("other");
    }
};

5.3. With a Custom Annotation

Another smart approach is to create a custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Exclude {}

We can then exploit ExclusionStrategy in order to make it work exactly as with the @Expose annotation, but inversely:

public class MyClass {
    private long id;
    private String name;
    @Exclude 
    private String other;
    private MySubClass subclass;
}

public class MySubClass {
    private long id;
    private String description;
    @Exclude 
    private String otherVerboseInfo;
}

And here is the strategy:

ExclusionStrategy strategy = new ExclusionStrategy() {
    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }

    @Override
    public boolean shouldSkipField(FieldAttributes field) {
        return field.getAnnotation(Exclude.class) != null;
    }
};

This StackOverflow answer firstly described this technique.

It allows us to write the annotation and the Strategy once, and to dynamically annotate our fields without further modification.

5.4. Extend Exclusion Strategy to Deserialization

No matter which strategy we’ll use, we can always control where it should be applied.

Only during serialization:

Gson gson = new GsonBuilder().addSerializationExclusionStrategy(strategy)

Only during deserialization:

Gson gson = new GsonBuilder().addDeserializationExclusionStrategy(strategy)

Always:

Gson gson = new GsonBuilder().setExclusionStrategies(strategy);

6. Conclusion

We’ve seen different ways to exclude fields from a class and its subclasses during Gson serialization.

We’ve also explored the main advantages and pitfalls of every solution.

As always, the full source code is available over on Github.

Course – LS (cat=JSON/Jackson)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.