Skip to content

Instantly share code, notes, and snippets.

@leedm777
Last active December 13, 2021 08:19
Show Gist options
  • Save leedm777/5730877 to your computer and use it in GitHub Desktop.
Save leedm777/5730877 to your computer and use it in GitHub Desktop.
Type variance in Swagger models

This document describes how to extend Swagger data models to allow the types of fields to vary.

Given that Swagger models need to map cleanly to a statically typed object model, a subclassing approach seems like it would be a good fit.

Inheritance

Inheritance allows a model (the derived type) to inherit all of the properties of another (the base type). This allows for a more compact representation of models, since common sets of fields may be extracted into a base type. This also allows the Swagger data model to match more closely to what an object model might look like.

Inheritance is transitive, meaning that if the base type inherits from another model, the derived type inherits those properties, too.

To declare an inheritance relationship between two models, a field extends is added to the derived type's model definition, which gives the id of the base type.

Only single inheritance is supported; meaning at most one extends field may be declared on a given model.

The derived type MUST NOT redefine any properties defined in any of its base types.

The base type named in an extends field SHOULD be defined within the same API declaration.

The extends relationship between models MUST NOT be cyclic.

Example

This example defines four models, with a fairly simple inheritance relationship between them.

         +--- Foo <--- Bar
         |
Base <---+
         |
         +--- Bam

Base is the base type, with a single property baseProp. Foo derives from Base, so it inherits the property baseProp, in addition to defining its own property fooProp. Bar derives from Foo, and inherits both baseProp and fooProp. Finally, Bam also derives from Base, so it inherits baseProp.

"models": {
  "Base": {
    "id": "Base",
    "properties": { "baseProp": { "type": "string" } }
  },
  "Foo": {
    "id": "Foo",
    "extends": "Base",
    "properties": { "fooProp": { "type": "string"} }
  },
  "Bar": {
    "id": "Bar",
    "extends": "Foo",
    "properties": { "barProp": { "type": "string"} }
  },
  "Bam": {
    "id": "Bam",
    "extends": "Base",
    "properties": { "bamProp": { "type": "string"} }
  }
}

Here are some example objects that would conform to this model:

Base - { "baseProp": "base" }
Foo  - { "baseProp": "base", "fooProp": "foo" }
Bar  - { "baseProp": "base", "fooProp": "foo", "barProp": "bar" }
Bam  - { "baseProp": "base", "bamProp": "bam" }

Polymorphism

In the above examples, there is no practical way for the client to determine the specific type of a received object. The ability to do so is very useful in being able to treat types polymorphically. If such a polymorphic base type is declared as the responseClass of an operation, the consumer can determine the type of object they are receiving.

To declare that a type may be treated polymorphically, a field discriminator is added to the type's model definition, which gives the id of the property to be used to identify the type of an object. The named field MUST be of type string, and MUST be required. The allowableValues for the property will be implicitly be the list of ids of all derived subtypes.

At most one discriminator field may be declared on a given model. Since the field is inherited by subtypes, they MUST NOT declare their own discriminator.

Subtypes are defined using the extends field, as described in the Inheritance section above.

Example

This example defines two classes to simply demonstrate the use of the discriminator field.

Animal <--- Cat

Animal is the base type, with two properties: id and dtype, with dtype identified as the discriminator. Cat extends Animal, and adds a name property.

"models": {
  "Animal": {
    "id": "Animal",
    "discriminator": "dtype",
    "properties": {
      "id": { "type": "long", },
      "dtype": { "type": "string", "required": true }
    }
  },
  "Cat": {
    "id": "Cat",
    "extends": Animal"
    "properties": {
      "name": { "type": "string" }
    }
  }
}

Since Animal declares a discriminator field, it may be treated polymorphically. If an operation's responseClass is Animal, it may safely return Animals or Cats.

Some valid objects that conforms to the Animal model:

{ "dtype": "Animal", "id": 8675309 }
{ "dtype": "Cat", "id": 3141593, "name": "Fluffy" }
@leedm777
Copy link
Author

@fehguy Identifying the discriminator in the model instead of properties maps would be fine.

I think you'll still need a field in the base model to be a discriminator, and that field should be a required string.

Here's an example. For clarity, I've named the discriminator field 'discr' instead of 'type'.

{
  "id": "BaseModel",
  "name": "BaseModel",
  "properties": {
    "discr": {
      "type": "string",
      "required": true
    },
    "id": {
      "type": "long",
      "required": false
    }
  },
  "description": "A cat model",
  "discriminator": "discr"
}

@fehguy
Copy link

fehguy commented Jun 26, 2013

I was thinking that this model hierarchy:

@ApiModel(value = "Cat", parent=BaseModel.class)
public class Cat extends BaseModel {
  private String name;
  ...
}

public abstract BaseModel {
  private Long id;
  ...
}

would be represented as such:

{
  "Cat": {
    "id": "Cat",
    "name": "Cat",
    "properties": {
      "name": {
        "type": "string",
        "required": false
      }
    },
    "description": "A cat model",
    "extends": "BaseModel"
  },
  "BaseModel": {
    "id": "BaseModel",
    "name": "BaseModel",
    "properties": {
      "id": {
        "type": "long",
        "required": false
      }
    },
    "type": "DISCRIMINATOR"
  }
}

Note how the Cat model extends BaseModel, which itself has type "DISCRIMINATOR". There is no type attribute in the properties. Is there some other reason that I'm missing that would make the type property helpful?

@fehguy
Copy link

fehguy commented Jun 26, 2013

Code to do this automatically has been pushed to 1.3.0:

swagger-api/swagger-core@e682859

In this sample:

https://github.com/wordnik/swagger-core/tree/develop-1.3/samples/scala-jaxrs-fileupload

You will see the Category class extends BaseModel:

@ApiModel(value="The Category Model", parent=classOf[BaseModel])
@XmlRootElement(name = "Category")
class Category() extends BaseModel {
  private var name:String = _

  @XmlRootElement(name = "name")
  def getName():String = name
  def setName(name:String) = this.name = name
}

abstract class BaseModel() {
  private var id:Long = 0

  @XmlElement(name="id")
  def getId():Long = id
  def setId(id:Long) = this.id = id
}

Which produces this json:

"Category": {
  "id": "Category",
  "name": "Category",
  "properties": {
    "name": {
      "type": "string",
      "required": false
    }
  },
  "description": "",
  "extends": "BaseModel"
},
"BaseModel": {
  "id": "BaseModel",
  "name": "BaseModel",
  "properties": {
    "id": {
      "type": "long",
      "required": false
    }
  },
  "type": "DISCRIMINATOR"
}

@leedm777
Copy link
Author

The type attribute is useful so that the receiver knows what type of object they are receiving. Without that, receivers won't be able to treat the object polymorphically.

For example, let's say you have an operation that has returnType: "BaseModel". It could respond with either of the following:

{ "id": 8675309, "name": "foo" }
{ "id": 3141593 }

The receiver has no idea whether the received object is a BaseModel or a Category.

What you're describing is inheritance without polymorphism. Which, for data modeling, might actually be useful (a set of field definitions to be shared across some model objects, for example). I can update the gist to take that use case into account.

But polymorphism (being able to receive a Category when the operation's return type is BaseModel) is necessary for my current use case. That's why a type field is useful.

And just a couple of points about specifying type: "DISCRIMINATOR".

Discriminator describes a field that serves to distinguish the type of a JSON object. It doesn't really make sense to call a base class a discriminator.

And for the non-polymorphic use case, from a data modeling perspective, you really shouldn't have to put anything on the BaseModel at all. The only extra information that the BaseModel really needs is which field to use as a discriminator (which is only needed if you want to treat the type polymorphically).

@fehguy
Copy link

fehguy commented Jun 26, 2013

I understand the use case better now. it sounds like two different things, as you mentioned above.

  1. Exposing a base class for models which allows a more compact representation of them, by removing shared fields. This has other benefits for code generation.

  2. Ideally a field in the model would point to a property, which would in turn tell the consumer what type of object they're receiving.
    #1 is the hard part (and it's done).
    #2 could be easy, depending. Take for example:

I'm modeling animals, and I have a Animal and a Cat. It's my job (as the api producer) to have Cat extend BaseAnimal. It is also my job as the api producer, to annotate BaseAnimal and define the discriminator, for example like such:

"Cat": {
  "id": "Cat",
  "name": "Cat",
  "properties": {
    "name": {
      "type": "string",
      "required": false
    }
  },
  "description": "",
  "extends": "BaseAnimal"
},
"Animal": {
  "id": "Animal",
  "name": "Animal",
  "properties": {
    "id": {
      "type": "long",
      "required": false
    },
    "type": {
      "type": "string",
      "required": true
    }
  },
  "discriminator": "type"
}

If my API has a method which returns "Animal", I may receive a "Cat" because it extends BaseAnimal. It is then my job to look at the field type to determine if I should unmarshall it as a Cat or other.

If that's the case (letting the API producer make these associations explicitly instead of automatically), then this is easy.

@leedm777
Copy link
Author

Cat should extend Animal, since that's the type name in the model. Other than that, it looks great.

@fehguy
Copy link

fehguy commented Jun 26, 2013

oops, yes, copy/paste error. I'll update this tonight.

@fehguy
Copy link

fehguy commented Jun 27, 2013

OK this has been pushed

@efuquen
Copy link

efuquen commented Aug 7, 2013

I've been looking for documentation on the extends and discriminator property, this is exactly what I've been looking for. Might I suggest this gets moved to a page on the wiki?

@frozenspider
Copy link

I also spend quite a while looking for this. Why there are little to no documentation on this? Also, why can't I make this work in definitions section of .yml file?

@ialmetwally
Copy link

is this feature implemented in the swagger codegen?

@himanshuy
Copy link

I do not think so. swagger codegen doesn't support polymorphic objects.

@natke
Copy link

natke commented Sep 4, 2016

How does the discriminator value get mapped to the actual subType?

e.g. if discriminator equals "type" and subTypes are {Cat.class, Dog.class}, where in the model does it map type=cat to Cat.class and type=dog to Dog.class?

@vglushonkov
Copy link

Please clarify if this is supported in Swagger 2.0, Swagger Editor does not understand "extends" field

@dobegor
Copy link

dobegor commented Feb 22, 2017

@fehguy, please tell us if this is supported in Swagger 2.0 because ^ (see above).
Thanks.

@pacey
Copy link

pacey commented Mar 8, 2017

How can polymorphic definitions be used in request and response definitions? If I have an endpoint for POST /v1/animals and I want to send a cat or a dog, how can this be described in the request definition? If I just reference the animal definition then I get warning that the cat and dog models are never used, which makes sense.

Secondly, if I want to be able to search all animals by GET /v1/animals this will return both cats and dogs. How can this be described in the response definition? Again, If I just reference the animal model then the cat and dog models aren't used.

@cimarron-pistoncloud
Copy link

I have the same question as @pacey.

@jeffersonchoi
Copy link

Same question as @pacey and @cimarron-pistoncloud.

@Berathiel
Copy link

I have the same question as @pacey, @cimarron-pistoncloud and @jeffersonchoi.
Any news on the issue?

@guillemdc
Copy link

Same question here as @Berathiel, @pacey, @cimarron-pistoncloud and @jeffersonchoi.

@dbaltor
Copy link

dbaltor commented May 15, 2018

I'm afraid that can only be done with Swagger 3.0 OneOf keyword in the request/response schema definition as per below:

https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment