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