- Notifications
You must be signed in to change notification settings - Fork 2.2k
OpenAPI 3.0 Schema Resolution
NOTE: Swagger Core 2.X produces OpenApi 3.x definition files. For more information, check out the OpenAPI specification repository. If you're looking for swagger 1.5.X and OpenApi 2.0, please refer to 1.5.X JAX-RS Setup
NOTE: The following DOES NOT apply to OpenAPI 3.1 (obtained by passing config option openapi31
, see wiki page)
One of the core capabilities of Swagger Core is to "resolve" 3.0 Schema constructs from Java types and annotations; in a scenario consisting in a JAX-RS application (e.g. Jersey or RESTEasy) with a resource like:
@Path("/employee") public class EmployeeResource { @POST public String addEmployee( @Schema(description = "Employee parameter") Employee employee) { return "ok"; } }
and related model POJOs:
@Schema(description = "Employee") public class Employee { private long id; private String name; private Address homeAddress = new Address(); private Address workAddress = new Address(); public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Address getWorkAddress() { return workAddress; } public void setWorkAddress(Address workAddress) { this.workAddress = workAddress; } public Address getHomeAddress() { return homeAddress; } public void setHomeAddress(Address homeAddress) { this.homeAddress = homeAddress; } }
public class Address { private String street; private String city; public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } }
the resolved spec will look like:
openapi: 3.0.1 paths: /employee: post: operationId: addEmployee requestBody: content: '*/*': schema: $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string components: schemas: Address: type: object properties: street: type: string city: type: string Employee: type: object properties: id: type: integer format: int64 name: type: string homeAddress: $ref: '#/components/schemas/Address' workAddress: $ref: '#/components/schemas/Address' description: Employee
This is the standard behavior, where e.g. the @Schema(description = "Employee parameter") Employee employee
parameter is resolved as a reference to an Object Schema Employee
bundled in components/schemas
.
... requestBody: content: '*/*': schema: $ref: '#/components/schemas/Employee' ... Employee: type: object properties: ... homeAddress: $ref: '#/components/schemas/Address' workAddress: $ref: '#/components/schemas/Address' description: Employee
This works fine for the majority of scenarios, however in some situations, given that OpenAPI 3.0 does not support $ref
siblings, information can get lost or even be wrong; an occurrence of lost information is already visible in the example above, where description
defined in @Schema(description = "Employee parameter") Employee employee
is not part of the resolved spec as the description part of the Employee
class application takes precedence:
... Employee: type: object ... description: Employee
If we slightly modify the above example to add more information via annotations, we can see that the issue gets more evident:
@Path("/employee") public class EmployeeResource { @POST public String addEmployee( @Schema(description = "Add Employee parameter", requiredProperties = {"name"}) Employee employee) { return "ok"; } @PUT public String updateEmployee( @Schema(description = "Update Employee parameter", requiredProperties = {"id", "name"}) Employee employee) { return "ok"; } }
public class Employee { private long id; private String name; private Address homeAddress = new Address(); private Address workAddress = new Address(); private BaseObject additionalData; private BaseObject metadata; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Schema(description = "Home address of the employee", nullable = true) public Address getWorkAddress() { return workAddress; } @Schema(description = "Work address of the employee") public void setWorkAddress(Address workAddress) { this.workAddress = workAddress; } public Address getHomeAddress() { return homeAddress; } public void setHomeAddress(Address homeAddress) { this.homeAddress = homeAddress; } @Schema(description = "Additional Data", properties = { @StringToClassMapItem(key = "name", value = String.class), @StringToClassMapItem(key = "address", value = Address.class), }) public BaseObject getAdditionalData() { return additionalData; } public void setAdditionalData(BaseObject additionalData) { this.additionalData = additionalData; } @Schema(description = "metadata") public BaseObject getMetadata() { return metadata; } public void setMetadata(BaseObject metadata) { this.metadata = metadata; } }
public class Address { private String street; private String city; public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } }
public class BaseObject { public String getId() { return id; } public void setId(String id) { this.id = id; } private String id; }
From the code above, with default behavior, the resolved spec will look like:
openapi: 3.0.1 paths: /employee: put: operationId: updateEmployee requestBody: content: '*/*': schema: $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string post: operationId: addEmployee requestBody: content: '*/*': schema: $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string components: schemas: Address: type: object properties: street: type: string city: type: string description: Home address of the employee nullable: true BaseObject: type: object properties: id: type: string description: metadata Employee: required: - id - name type: object properties: id: type: integer format: int64 name: type: string homeAddress: $ref: '#/components/schemas/Address' workAddress: $ref: '#/components/schemas/Address' additionalData: $ref: '#/components/schemas/BaseObject' metadata: $ref: '#/components/schemas/BaseObject' description: Update Employee parameter
We can spot various issues here, where information gets lost or applied wrongly, among with:
- Required fields
id
andname
are incorrectly added to the bundledEmployee
object, used by bothput
andpost
, - 'description' is incorrectly added to the bundled
Employee
object, used by bothput
andpost
, with the value "coming" fromput
-
additionalData
properties added via annotations are dropped
In order to overcome this kind of problems, since version 2.2.24 configuration property schemaResolution
is available.
It allows to specify how object schemas and object properties within schemas are resolved for OAS 3.0 specification:
DEFAULT
: object schemas are bundled into components/schemas and schemas or properties referring to them are resolved as Reference Schema ( with $ref
field populated and no other fields). This is the default when config property is not provided and in versions < 2.2.24. It can causes the issues detailed above.
ALL_OF
: object schemas are bundled into components/schemas and schemas or properties referring to them are resolved as Reference Schema in an item of an allOf
array field, along with any sibling fields resolved into a second allOf
array item.
ALL_OF_REF
: object schemas are bundled into components/schemas and schemas or properties referring to them are resolved as Reference Schema in an item of an allOf
array field, along with any sibling fields resolved into the parent schema.
Applying ALL_OF
and ALL_OF_REF
results in two similar schemas with a slight difference in behavior and tooling support, which is the reason why both options are allowed.
INLINE
: object schemas are resolved into "inline" schemas, therefore not referencing schemas in components/schemas.
NOTE: INLINE
can cause unexpected results or errors and is not recommended
The results of processing the example resource above applying the different values for schemaResolution
configuration property are provided here below:
openapi: 3.0.1 paths: /employee: put: operationId: updateEmployee requestBody: content: '*/*': schema: allOf: - required: - id - name description: Update Employee parameter - $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string post: operationId: addEmployee requestBody: content: '*/*': schema: allOf: - required: - name description: Add Employee parameter - $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string components: schemas: Address: type: object properties: street: type: string city: type: string BaseObject: type: object properties: id: type: string Employee: type: object properties: id: type: integer format: int64 name: type: string homeAddress: $ref: '#/components/schemas/Address' workAddress: allOf: - description: Home address of the employee nullable: true - $ref: '#/components/schemas/Address' additionalData: allOf: - properties: name: type: string address: $ref: '#/components/schemas/Address' description: Additional Data - $ref: '#/components/schemas/BaseObject' metadata: allOf: - description: metadata - $ref: '#/components/schemas/BaseObject'
openapi: 3.0.1 paths: /employee: put: operationId: updateEmployee requestBody: content: '*/*': schema: required: - id - name description: Update Employee parameter allOf: - $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string post: operationId: addEmployee requestBody: content: '*/*': schema: required: - name description: Add Employee parameter allOf: - $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string components: schemas: Address: type: object properties: street: type: string city: type: string BaseObject: type: object properties: id: type: string Employee: type: object properties: id: type: integer format: int64 name: type: string homeAddress: $ref: '#/components/schemas/Address' workAddress: description: Home address of the employee nullable: true allOf: - $ref: '#/components/schemas/Address' additionalData: properties: name: type: string address: $ref: '#/components/schemas/Address' description: Additional Data allOf: - $ref: '#/components/schemas/BaseObject' metadata: description: metadata allOf: - $ref: '#/components/schemas/BaseObject'
openapi: 3.0.1 paths: /employee: put: operationId: updateEmployee requestBody: content: '*/*': schema: required: - id - name type: object properties: id: type: integer format: int64 name: type: string homeAddress: type: object properties: street: type: string city: type: string workAddress: type: object properties: street: type: string city: type: string description: Home address of the employee nullable: true additionalData: type: object properties: name: type: string address: $ref: '#/components/schemas/Address' description: Additional Data metadata: type: object properties: id: type: string description: metadata description: Update Employee parameter responses: default: description: default response content: '*/*': schema: type: string post: operationId: addEmployee requestBody: content: '*/*': schema: required: - name type: object properties: id: type: integer format: int64 name: type: string homeAddress: type: object properties: street: type: string city: type: string workAddress: type: object properties: street: type: string city: type: string description: Home address of the employee nullable: true additionalData: type: object properties: name: type: string address: $ref: '#/components/schemas/Address' description: Additional Data metadata: type: object properties: id: type: string description: metadata description: Add Employee parameter responses: default: description: default response content: '*/*': schema: type: string components: schemas: Address: type: object properties: street: type: string city: type: string
Another more granular way to influence the outcome of Object Schema resolution is using @Schema.schemaResolution
, adding the value to a @Schema
annotation applied to a class and/or a member/getter (therefore NOT applicable to e.g. a parameter annotation). If set, the value of @Schema.schemaResolution
takes precedence over the globally applied configuration parameter. In this way it is possible to have particular schemas resolved with their own strategy.
If we modify the Employee
class by adding schemaResolution
to @Schema
annotations applied to class members, keeping e.g. the config property (applied globally) , the outcome will differ for the schemas to which the annotation was applied:
public class Employee { private long id; private String name; private Address homeAddress = new Address(); private Address workAddress = new Address(); private BaseObject additionalData; private BaseObject metadata; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Schema( description = "Home address of the employee", nullable = true, schemaResolution = Schema.SchemaResolution.ALL_OF_REF) public Address getWorkAddress() { return workAddress; } @Schema(description = "Work address of the employee") public void setWorkAddress(Address workAddress) { this.workAddress = workAddress; } public Address getHomeAddress() { return homeAddress; } public void setHomeAddress(Address homeAddress) { this.homeAddress = homeAddress; } @Schema(description = "Additional Data", properties = { @StringToClassMapItem(key = "name", value = String.class), @StringToClassMapItem(key = "address", value = Address.class), }) public BaseObject getAdditionalData() { return additionalData; } public void setAdditionalData(BaseObject additionalData) { this.additionalData = additionalData; } @Schema(description = "metadata", schemaResolution = Schema.SchemaResolution.INLINE) public BaseObject getMetadata() { return metadata; } public void setMetadata(BaseObject metadata) { this.metadata = metadata; } }
will resolve into:
openapi: 3.0.1 paths: /employee: put: operationId: updateEmployee requestBody: content: '*/*': schema: allOf: - required: - id - name description: Update Employee parameter - $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string post: operationId: addEmployee requestBody: content: '*/*': schema: allOf: - required: - name description: Add Employee parameter - $ref: '#/components/schemas/Employee' responses: default: description: default response content: '*/*': schema: type: string components: schemas: Address: type: object properties: street: type: string city: type: string BaseObject: type: object properties: id: type: string description: metadata Employee: type: object properties: id: type: integer format: int64 name: type: string homeAddress: $ref: '#/components/schemas/Address' workAddress: description: Home address of the employee nullable: true allOf: - $ref: '#/components/schemas/Address' additionalData: allOf: - properties: name: type: string address: $ref: '#/components/schemas/Address' description: Additional Data - $ref: '#/components/schemas/BaseObject' metadata: type: object properties: id: type: string description: metadata