Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update resources with patch method using OpenAPI correctly?

I am learning OpenAPI recently, and would like to know the best practices. Let's say I have a resource called Person, and it is defined in components/schemas as follows:

Person:
  type: object
  required:
    - id
    - name
    - age
  properties:
    id:
      readOnly: true
      type: integer
    name:
      type: string
    age:
      type: integer

I've made id readOnly because when I do post or patch, the ID will be passed as part of the URL. See https://swagger.io/docs/specification/data-models/data-types/

name and age must present when the client tries to create a new person using post method, or get a person, therefore they are defined as required.

My question is about patch: what if I only want to update a person's age or name independently? Ideally I would like to do something like

PATCH /person/1

{"age": 40}

However, since I've defined name as required, I can't do it. I can think of several solutions, but they all have flaws:

  1. Remove the required restriction. But if I do that, I lose the validation on post and get.
  2. Use a separate schema for patch, e.g. PersonUpdate, with required removed. Apparently that leads to redundancy.
  3. When I do patch, I do pass all the fields, but for the ones I don't want to update, I pass an invalid value, e.g.
PATCH /Person/1

{"age": 40, "name": null}

And make all the fields nullable, and let the server ignore these values. But what if I do want to set name to null in DB?

  1. I use PUT for update, and always pass all the required fields. But what if my data is outdated? E.g. when I do
PUT /Person/1

{"age": 40, "name": "Old Name"}

Some other client has already changed name to "New Name", and I am overriding it.

  1. Like method 3, but I pass additional fields when doing patch to indicate the fields the server should care, whether using query parameters like ?fields=age, or add it to the JSON body. So I can change the requestBody to something like
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/Person'
                - type: object
                  properties:
                    _fields:
                      type: array
                      items:
                        type: string

Then I can do this

PATCH /Person/1

{"age": 40, "name": null, _fields: ["age"]}

In this way, I can update name to null as well

PATCH /Person/1

{"age": 40, "name": null, _fields: ["age", "name"]}

This method seems can work, but is there a better or widely accepted practice?

like image 506
DrizzleX Avatar asked Jun 13 '20 10:06

DrizzleX


2 Answers

I came up with the following solution:

Person:
  type: object
  allOf:
    - $ref: '#/components/schemas/PersonProperties'
    - required:
      - id
      - name
      - age
UpdatePerson:
  type: object
  allOf:
    - $ref: '#/components/schemas/PersonProperties'
PersonProperties:
  type: object
  properties:
    id:
      readOnly: true
      type: integer
    name:
      type: string
    age:
      type: integer

PersonProperties acts as a simple collection of properties which make up a the person model. It doesn't specify any required fields.

Person re-uses all properties of PersonProperties and, in addition, specifies which one are required. PersonUpdate re-uses all properties of PersonProperties and allows for partial updates.

This solution still feels hacky. Also, it doesn't work for partial updates of nested objects and nullable properties.

(In fact, since the id is readOnly, you could also add required: [id] on PersonProperties and remove id from the required list on Person)

like image 178
mulles3008 Avatar answered Oct 10 '22 20:10

mulles3008


By splitting the schemas up into multiple, composable schemas, we can layer the schemas on top of one another without repeating ourselves. We can even support removal of properties via PATCH by making optional properties nullable.

Example OpenAPI definition allowing partial updates via PATCH endpoints (validated):

openapi: 3.0.0

info:
  title: Sample API with reusable schemas and partial updates (PATCH)
  version: 1.0.0

paths:
  /customers:
    post:
      tags:
        - Customer
      requestBody:
        $ref: '#/components/requestBodies/CreateCustomer'
      responses:
        201:
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerId'
    get:
      tags:
        - Customer
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Customer'
                  
  /customers/{CustomerId}:
    get:
      tags:
        - Customer
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
                  
    put:
      tags:
        - Customer
      requestBody:
        $ref: '#/components/requestBodies/CreateCustomer'
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        204:
          description: Updated
          
    patch:
      tags:
        - Customer
      requestBody:
        description: Update customer with properties to be changed
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/CustomerProperties'
                - type: object
                  properties:
                    Segment:
                      nullable: true
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        204:
          description: Updated

components:
  schemas:
    CustomerProperties:
      type: object
      properties:
        FirstName:
          type: string
        LastName:
          type: string
        DOB:
          type: string
          format: date-time
        Segment:
          type: string
          enum:
            - Young
            - MiddleAged
            - Old
            - Moribund

    CustomerRequiredProperties:
      type: object
      required:
        - FirstName
        - LastName
        - DOB

    Id:
      type: integer
      
    CustomerId:
      type: object
      properties:
        Id:
          $ref: '#/components/schemas/Id'
          
    Customer:
      allOf:
        - $ref: '#/components/schemas/CustomerId'
        - $ref: '#/components/schemas/CustomerProperties'
        - $ref: '#/components/schemas/CustomerRequiredProperties'
   
  parameters:
    CustomerId:
      name: CustomerId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Id'
        
  requestBodies:
    CreateCustomer:
      description: Create a new customer
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/CustomerProperties'
              - $ref: '#/components/schemas/CustomerRequiredProperties'

Adapted from: https://stoplight.io/blog/reuse-openapi-descriptions/

like image 35
chrisjsherm Avatar answered Oct 10 '22 20:10

chrisjsherm