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 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.
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" }
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.
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 Animal
s or Cat
s.
Some valid objects that conforms to the Animal
model:
{ "dtype": "Animal", "id": 8675309 }
{ "dtype": "Cat", "id": 3141593, "name": "Fluffy" }
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:The receiver has no idea whether the received object is a
BaseModel
or aCategory
.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 isBaseModel
) 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 theBaseModel
really needs is which field to use as a discriminator (which is only needed if you want to treat the type polymorphically).