swagger-api / swagger-api/swagger-codegen

Polymorphic JSON Object mapping with Gson (Android Client)

Open
#5,580 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Client: Android Feature: Composition / Inheritance help wanted Issue: Bug
Dominant language
Mustache
Stars
17.8k
Forks
6k
PR merge metrics
No merged PRs in 30d

Description

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!

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the generated Java model classes and JsonUtil.java, then compare the relevant Mustache templates against the supplied Device/Function/OnOff schema. Reproduce polymorphic Gson deserialization and RuntimeTypeAdapterFactory serialization; done means inherited function_id is not emitted twice and both operations work without the separate-Gson workaround.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, java
Domain
mobile-dev, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.