Skip to content

Polymorphic JSON Object mapping with Gson (Android Client) #5580

@philipptrenz

Description

@philipptrenz
Description

I've tried to get the Android Client to run with polymorphic JSON deserialization and stumbled across some issues.

Swagger-codegen version

From editor.swagger.io (new version, two days ago)

Swagger declaration file content or url

Here the swagger definition to the Objects:

definitions:
  Device:
    type: object
    properties:
      device_id:
        type: string
      type_id:
        type: string
      room_ids:
        type: array
        items:
          type: string
      group_ids:
        type: array
        items:
          type: string
      functions:
        type: array
        items:
          $ref: '#/definitions/Function'
    required:
      - device_id
      - room_ids
      - group_ids
      - functions
  Function:
    description: Base Class for all functions
    type: object
    properties:
      function_id:
        type: string
    required:
      - function_id
  OnOff:
    description: Generic On-Off switch
    allOf:
      - $ref: '#/definitions/Function'
      - properties:
          isOn:
            type: boolean
            default: false
          timestamp:
            type: string
            format: date-time
  Dimmer:
    description: Generic slider switch
    allOf:
      - $ref: '#/definitions/Function'
      - properties:
          value:
            type: integer
            minimum: 0
            maximum: 255
            default: 0
          timestamp:
            type: string
            format: date-time
            
  Temperature:
    description: Generic temperature sensor
    allOf:
      - $ref: '#/definitions/Function'
      - properties:
          value:
            description: Temperature value in Kelvin
            type: number
            minimum: 0
            maximum: 511
            default: 0
          timestamp:
            type: string
            format: date-time  

Then the models have all the type 'function_id'

Steps to reproduce

My research has shown, Gson is not able to do polymorphic mapping out of the box, but the only in the repository provided TypeAdapter RuntimeTypeAdapterFactory.java can do that by adding the class to the project and enhancing the generated JsonUtil.java like this:

static {
  final RuntimeTypeAdapterFactory<Function> typeFactory = RuntimeTypeAdapterFactory
          .of(Function.class, "function_id") // Define the parent class and the identifier key for each subclass
          .registerSubtype(OnOff.class, "onoff")
          .registerSubtype(Dimmer.class, "dimmer")
          .registerSubtype(Temperature.class, "temperature");

  gsonBuilder = new GsonBuilder();
  gsonBuilder.registerTypeAdapterFactory(typeFactory);

  [...]
}

But now I got a JsonParseException: cannot serialize Function because it already defines a field named onoff. Then I looked inside the models and saw this:

public class Function {
  
  @SerializedName("function_id")
  private String functionId = null;

  /**
   **/
  @ApiModelProperty(required = true, value = "")
  public String getFunctionId() {
    return functionId;
  }
  public void setFunctionId(String functionId) {
    this.functionId = functionId;
  }

  [...]

}

@ApiModel(description = "Generic On-Off switch")
public class OnOff extends Function {
  
  @SerializedName("function_id")
  private String functionId = null;
  @SerializedName("isOn")
  private Boolean isOn = null;
  @SerializedName("timestamp")
  private Date timestamp = null;

  /**
   **/
  @ApiModelProperty(required = true, value = "")
  public String getFunctionId() {
    return functionId;
  }
  public void setFunctionId(String functionId) {
    this.functionId = functionId;
  }

  [...]


  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    OnOff onOff = (OnOff) o;
    return (this.functionId == null ? onOff.functionId == null : this.functionId.equals(onOff.functionId)) &&
        (this.isOn == null ? onOff.isOn == null : this.isOn.equals(onOff.isOn)) &&
        (this.timestamp == null ? onOff.timestamp == null : this.timestamp.equals(onOff.timestamp));
  }

  @Override
  public int hashCode() {
    int result = 17;
    result = 31 * result + (this.functionId == null ? 0: this.functionId.hashCode());
    result = 31 * result + (this.isOn == null ? 0: this.isOn.hashCode());
    result = 31 * result + (this.timestamp == null ? 0: this.timestamp.hashCode());
    return result;
  }

  @Override
  public String toString()  {
    StringBuilder sb = new StringBuilder();
    sb.append("class OnOff {\n");
    sb.append("  " + super.toString()).append("\n");
    sb.append("  functionId: ").append(functionId).append("\n");
    sb.append("  isOn: ").append(isOn).append("\n");
    sb.append("  timestamp: ").append(timestamp).append("\n");
    sb.append("}\n");
    return sb.toString();
  }
}
Suggest a Fix

So the subclasses have their own function_id class identifier key and the parent class gets just nested inside the subclass from swagger codegen. After deleting the subclass property String functionId and changing every 'this.functionId' to 'super.getFunctionId()' the polymorphic mapping with RuntimeTypeAdapterFactory.java worked fine!
I'm not sure if changing this in codegen could have side effects to non-polymorphic Gson mapping, but this looks obviously bad ;)

Deserialization now works for me, but there is still a issue with serializing objects with the registered RuntimeTypeAdapter. For me the workaround using a second Gson object for serialization where the RuntimeTypeAdapter isn't registered works so far.

I hope this will help to improve the codegen project because I'm very enthusiastic about it!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions