-
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