Last updated: May 1, 2025
Working with JSON in Java often requires customizing serialization behavior, and Gson makes this process relatively straightforward. However, excluding specific fields from serialization, especially nested properties like country.name
, can be challenging when you want to avoid annotations. In this guide, we’ll explore effective techniques for excluding fields from Gson serialization without modifying your model classes.
Table of Contents
- Understanding the Challenge
- The Common Use Case
- Solution 1: Custom Exclusion Strategy
- Solution 2: Using ExclusionStrategy with Path Patterns
- Solution 3: Dynamic Field Exclusion
- Solution 4: Type Adapters Approach
- Performance Considerations
- When to Use Annotations Instead
- Conclusion
Understanding the Challenge
When working with Gson, most examples show field exclusion using annotations like @Expose
or the transient
keyword. But what if you’re working with third-party classes or want to apply exclusion patterns dynamically without modifying your model? Let’s dive into some practical solutions.
First, let’s look at a typical model structure that presents this challenge:
public class Student {
private Long id;
private String firstName = "Philip";
private String middleName = "J.";
private String initials = "P.F";
private String lastName = "Fry";
private Country country;
private Country countryOfBirth;
}
public class Country {
private Long id;
private String name;
private Object other;
}
The Common Use Case
A common requirement is to exclude specific fields from serialization, like firstName
or specifically country.name
but not countryOfBirth.name
. Standard exclusion strategies don’t handle this path-based exclusion easily, but we’ll show you how to overcome this limitation.
Solution 1: Custom Exclusion Strategy
The most straightforward approach is to implement a custom ExclusionStrategy
that checks both the declaring class and field name:
public class CustomExclusionStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
@Override
public boolean shouldSkipField(FieldAttributes field) {
return (field.getDeclaringClass() == Student.class && field.getName().equals("firstName")) ||
(field.getDeclaringClass() == Country.class && field.getName().equals("name"));
}
}
This strategy excludes all instances of name
from any Country
object, which isn’t exactly what we want if we need to exclude only country.name
but keep countryOfBirth.name
.
Usage:
Gson gson = new GsonBuilder()
.setExclusionStrategies(new CustomExclusionStrategy())
.create();
String json = gson.toJson(student);
Solution 2: Using ExclusionStrategy with Path Patterns
For more precise control, we can create a solution that mimics the behavior of regex pattern matching similar to what Struts2 JSON plugin offers:
public class PathExclusionStrategy implements ExclusionStrategy {
private final Set<String> excludePatterns;
private final ThreadLocal<Stack<String>> pathStack = ThreadLocal.withInitial(Stack::new);
public PathExclusionStrategy(String... patterns) {
this.excludePatterns = new HashSet<>(Arrays.asList(patterns));
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
@Override
public boolean shouldSkipField(FieldAttributes field) {
String currentPath = getCurrentPath(field.getName());
for (String pattern : excludePatterns) {
if (matchesPattern(currentPath, pattern)) {
return true;
}
}
return false;
}
private String getCurrentPath(String fieldName) {
if (pathStack.get().isEmpty()) {
return fieldName;
}
return pathStack.get().peek() + "." + fieldName;
}
private boolean matchesPattern(String path, String pattern) {
// Simple implementation - can be enhanced with actual regex
return path.equals(pattern) ||
(pattern.endsWith(".*") && path.startsWith(pattern.substring(0, pattern.length() - 2)));
}
// These methods would be called from a custom TypeAdapter Factory
public void pushPath(String path) {
pathStack.get().push(path);
}
public void popPath() {
if (!pathStack.get().isEmpty()) {
pathStack.get().pop();
}
}
}
However, this approach requires additional integration with a custom TypeAdapterFactory
to track the object path during serialization, which is beyond the scope of this article but represents a powerful solution for complex scenarios.
Solution 3: Dynamic Field Exclusion
Another approach is to use a TypeAdapter
with a predefined list of fields to exclude:
public class FieldExclusionTypeAdapterFactory implements TypeAdapterFactory {
private final Map<Class<?>, Set<String>> excludedFields = new HashMap<>();
public void excludeField(Class<?> type, String fieldName) {
excludedFields.computeIfAbsent(type, k -> new HashSet<>()).add(fieldName);
}
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// Get the default adapter
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
// Only apply custom handling if we have exclusions for this type
if (!excludedFields.containsKey(type.getRawType())) {
return delegate;
}
// Create our custom adapter
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
// Begin the object
out.beginObject();
// Get all fields of the type
Field[] fields = type.getRawType().getDeclaredFields();
Set<String> excluded = excludedFields.get(type.getRawType());
for (Field field : fields) {
// Skip excluded fields
if (excluded.contains(field.getName())) {
continue;
}
// Make private fields accessible
field.setAccessible(true);
try {
// Write field name
out.name(field.getName());
// Get field value and use appropriate adapter
Object value = field.get(value);
TypeAdapter adapter = gson.getAdapter(field.getType());
adapter.write(out, value);
} catch (IllegalAccessException e) {
throw new JsonIOException("Failed to access field", e);
}
}
// End the object
out.endObject();
}
@Override
public T read(JsonReader in) throws IOException {
// Use default deserialization for simplicity
return delegate.read(in);
}
};
}
}
Usage example:
FieldExclusionTypeAdapterFactory factory = new FieldExclusionTypeAdapterFactory();
factory.excludeField(Student.class, "firstName");
factory.excludeField(Country.class, "name");
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(factory)
.create();
Solution 4: Type Adapters Approach
For specific classes, you can create custom TypeAdapter
implementations:
TypeAdapter<Student> studentAdapter = new TypeAdapter<Student>() {
@Override
public void write(JsonWriter out, Student student) throws IOException {
out.beginObject();
// Write all fields except excluded ones
out.name("id").value(student.getId());
out.name("middleName").value(student.getMiddleName());
out.name("initials").value(student.getInitials());
out.name("lastName").value(student.getLastName());
// Custom handling for country - exclude name
if (student.getCountry() != null) {
out.name("country");
out.beginObject();
out.name("id").value(student.getCountry().getId());
// Deliberately skip the name field
out.endObject();
}
// Include full countryOfBirth
if (student.getCountryOfBirth() != null) {
out.name("countryOfBirth");
out.beginObject();
out.name("id").value(student.getCountryOfBirth().getId());
out.name("name").value(student.getCountryOfBirth().getName());
out.endObject();
}
out.endObject();
}
@Override
public Student read(JsonReader in) throws IOException {
// Implementation for deserialization
// ...
}
};
Gson gson = new GsonBuilder()
.registerTypeAdapter(Student.class, studentAdapter)
.create();
While more verbose, this approach gives you precise control over the serialization process.
Performance Considerations
When implementing custom exclusion strategies or type adapters, consider the performance impact:
- Simple exclusion strategies are fast but limited in functionality
- Path-based exclusion adds overhead but provides greater flexibility
- Custom type adapters offer the most control but require manual maintenance
For applications with high-volume JSON processing, benchmark your solution to ensure it meets performance requirements.
When to Use Annotations Instead
While this article focuses on non-annotation approaches, it’s worth noting when annotations might be the better choice:
- When you control the model classes and can modify them
- For simpler use cases where
@Expose
ortransient
is sufficient - When you want to avoid the complexity of custom serialization logic
The annotation-based approach is more maintainable in many scenarios:
@Expose(serialize = false)
private String firstName;
Used with:
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.create();
Conclusion
Excluding specific fields from Gson serialization without annotations is achievable through several techniques, from simple exclusion strategies to more complex type adapters. The best approach depends on your specific requirements and constraints.
For dynamic path-based exclusion similar to Struts2 patterns, a combination of custom ExclusionStrategy
and TypeAdapterFactory
provides the most flexible solution, though it requires more implementation effort.
Remember that while avoiding annotations offers greater flexibility for third-party classes and dynamic exclusion patterns, annotations often provide a cleaner and more maintainable solution when available.
What approach do you use for customizing Gson serialization? Share your experience in the comments below!
Need help with other Java serialization challenges? Check out our related articles on Working with Jackson and JSON processing in Spring Boot.