OpenAPI: Design a reusable schema definition for GET, PUT, POST
Asked Answered
A

1

10

I am using OpenAPI 3.0 to design and implement a simple API to allow basic CRUD operations on database entities.

Let's assume an entity Pet with some client-given properties (name & age) and some generated properties (id & created):

components:
  schemas:
    Pet:
      type: object
      properties:
        id:
          type: string
        created:
          type: string
          format: date-time
        name:
          type: string
        age:
          type: integer

I want to specify REST endpoints to POST, GET, PUT (and if possible PATCH) a pet:

paths:

  /pets:
    post:
      operationId: createPet
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pet"
      responses:
        "200":
          content:
            application/json:
              schema:
                type: boolean
 
  /pets/{id}:
    parameters:
      - name: id
        schema:
          type: string
        in: path
        required: true

    get:
      operationId: getPet
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"

    put:
      operationId: updatePet
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pet"
      responses:
        "200":
          content:
            application/json:
              schema:
                type: boolean

    patch:
      operationId: alterPet
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pet"
      responses:
        "200":
          content:
            application/json:
              schema:
                type: boolean

From a logical point of view, the following properties are required per endpoint:

  • GET: id, created, name, age
  • POST: name, age
  • PUT: name, age
  • PATCH: name or age (or none depending on implementation)

I see two major approaches here:

Approach 1: Leave all properties optional

The Pet DTO is used as a shell where all properties are optional as defined above. It is left to the server & client to check if the required properties are filled on an endpoint call. If non-required properties are set in a POST/PUT/PATCH request body, they will be ignored.

pros:

  • Single, simple, and symmetric DTO schema definition in the API specification.
  • Smooth integration of the "GET -> modify -> PUT" workflow on the client side (e.g., a React frontend with Formik).
  • Supports the PATCH endpoint out-of-the-box.

cons:

  • All properties are optional and it is very tedious to handle these string | undefined types in Typescript. This is especially annoying in the GET direction as we know that all properties will be filled as long as we retrieve the Pet DTO.
  • The implementational "pain" is spread over the server and all possible clients.

Approach 2: Define separate DTO schemas for all operations

We introduce a GetPet, PutPet, PostPet, and PatchPet with corresponging required lists in the API specification. Yes, PutPet and PostPet can be identical, but maybe we want to allow a modified GetPet to be used as PutPet to streamline the "GET -> modify -> PUT" workflow for the client.

Imagine the Pet entity having 5+ generated properties and 20+ client-given properties. We do not want to flat define 4 variations of each entity, but use some kind of inheritance instead. Is there a way to introduce a BasePet bearing all properties (set to optional) and let the 4 operation-specific DTOs extend it with just overriding the required list?

pros:

  • Easier implementation for client & server as optionality is clarified for all properties.
  • The "pain" is handled in the API specification.

cons:

  • The API specification grows in complexity. It is not clear to me if the desired inhertance is possible and how it is specified.
  • Scalability & maintainability of the API may suffer.

So my question on this topic: How can the inhertiance of the DTOs be specified for OpenAPI 3.0? Are there better alternatives? I am happy for all suggestions concerning these thoughts on an API for basic CRUD operations.

Accelerant answered 8/2, 2021 at 13:37 Comment(2)
Does this answer your question? Re-using model with different required propertiesErrolerroll
@Errolerroll probably not, as the schema is the same for read and write, and this is different from the requirement of OP.Cuspidation
C
1

I did face a similar issue while designing an API for a client recently. The trick here is to $ref: redundancies in the schemata for the different routes. For simplicity, I will only define GET, POST and PATCH.

Note: In this case it is easier, as the schema for write operations is a subset of the schema for the read operation.

By contrast, imagine a user-object, where there is a password field, it should be writable, but not readable. In this case, you would need three different schemata:

  • userRead
  • userWrite
  • userCommonFieldsForReadAndWrite

In any case, here is one possible solution to your question:

paths:
  /pets:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/petCreateUpdate"
      responses:
        "201":
          description: Pet created.
          headers:
            Location:
              schema:
                type: string
              description: points to the Pet resource created. Can be directly used in a subsequent GET request.
              example: "Location: /pets/32"
  /pets/{id}:
    parameters:
      - name: id
        schema:
          type: string
        in: path
        required: true
    get:
      responses:
        "200":
          description: Pet with given id returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/petRead"
        "404":
          description: Pet with given id not found.
    patch:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/petCreateUpdate"
      responses:
        "204":
          description: Pet updated successfully.

components:
  schemas:

    petRead:
      allOf:
        - type: object
          properties:
            id:
              type: integer
              format: int32
              minimum: 1
            created:
              type: string
              format: date-time
        - $ref: "#/components/schemas/petCreateUpdate"

    petCreateUpdate:
      type: object
      properties:
        name:
          type: string
        age:
          type: number
          format: integer
          minimum: 1
          maximum: 100

This renders in SwaggerEditor as follows:

Cuspidation answered 22/1, 2023 at 12:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.