Skip to content

OpenAPI 3.0 Schema Resolution

Francesco Tumanischvili edited this page Oct 8, 2024 · 1 revision

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


OpenAPI 3.0 Object Schema Resolution

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

A more complex scenario

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 and name are incorrectly added to the bundled Employee object, used by both put and post,
  • 'description' is incorrectly added to the bundled Employee object, used by both put and post, with the value "coming" from put
  • additionalData properties added via annotations are dropped

Configuration property schemaResolution

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_REFresults 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:

schemaResolution = ALL_OF

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' 

schemaResolution = ALL_OF_REF

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' 

schemaResolution = INLINE

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 

Setting schemaResolution of @Schema Annotation (in class or members/getters annotations)

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 
Clone this wiki locally