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:
required
restriction. But if I do that, I lose the validation on post
and get
.patch
, e.g. PersonUpdate
, with required
removed. Apparently that leads to redundancy.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?
PUT
for update, and always pass all the required fields. But what if my data is outdated? E.g. when I doPUT /Person/1
{"age": 40, "name": "Old Name"}
Some other client has already changed name
to "New Name", and I am overriding it.
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?
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
)
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With