swagger-api / swagger-api/swagger-codegen
Polymorphic JSON Object mapping with Gson (Android Client)
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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