Gson Field Exclusion Without Annotations: The Complete Guide

Gson Field Exclusion Without Annotations

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:

  1. Simple exclusion strategies are fast but limited in functionality
  2. Path-based exclusion adds overhead but provides greater flexibility
  3. 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:

  1. When you control the model classes and can modify them
  2. For simpler use cases where @Expose or transient is sufficient
  3. 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.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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