Skip to content

Serialize bean fields in class inheritance order #2309

@knoan

Description

@knoan

Problem solved by the feature

Consider the following skeleton resource model to be exposed through a REST/JSON API:

public abstract static class BaseResource {
    public String id;
}

public static final class SpecificResource extends BaseResource {
    public String data;
}

With respect to output readability, especially when models grow large, it would be very convenient to have fields serialized in definition order, as:

{
      "id": "/resources/specific/123",
      "data": "resource payload"
}

However, Gson currently serializes them backward, as:

{
      "data": "resource payload",
      "id": "/resources/specific/123"
}

Quick test with:

public static void main(final String... args) {

    final SpecificResource resource=new SpecificResource();

    resource.id="/resources/specific/123";
    resource.data="resource payload";

    System.out.println(new GsonBuilder()
            .setPrettyPrinting()
            .create()
            .toJson(resource)
    );

}

Feature description

Reverse class scanning order in:

private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw,

The current code scans classes upwards, accumulating fields in a ordered LinkedHashMap along the lines of:

private Map<String, ReflectiveTypeAdapterFactory.BoundField> getBoundFields(
  Gson context, 
  TypeToken<?> type, Class<?> raw, 
  boolean blockInaccessible, boolean isRecord
) {

    Map<String, ReflectiveTypeAdapterFactory.BoundField> result = new LinkedHashMap<>();
    
    while (raw != Object.class) {

        // …

        result.put(name, boundField);

        // …

        type = TypeToken.get(
          $Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
        raw = type.getRawType();

    }

    return result;

}

Downward class scanning could be easily performed with the assistance of a recursive accumulator method, like for instance:

private Map<String, ReflectiveTypeAdapterFactory.BoundField> getBoundFields(
        Gson context, 
        TypeToken<?> type, Class<?> raw, 
        boolean blockInaccessible, boolean isRecord
) {
    
    return getBoundFields(context, type, raw, blockInaccessible, isRecord, new LinkedHashMap<>());

}


private Map<String, ReflectiveTypeAdapterFactory.BoundField> getBoundFields(
        Gson context,
        TypeToken<?> type, Class<?> raw,
        boolean blockInaccessible, boolean isRecord,
        Map<String, ReflectiveTypeAdapterFactory.BoundField> result // ‹‹‹‹ accumulator
) {
    
    if (raw != Object.class) {
        
        // scan superclass

        TypeToken<?> superType = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
        Class<?> superRaw = superType.getRawType();

        getBoundFields(context, superType, superRaw, blockInaccessible, isRecord, result);

        
        // scan current class
        
        // …

        result.put(name, boundField);

        // …

    }

    return result;

}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions