1. Probleem
In onze API-richtlijnen ontbreekt nu een eenduidige afspraak over het verschil
tussen een property die ontbreekt en een property met de waarde null. Daardoor
kan dezelfde payload op verschillende manieren worden geïnterpreteerd door
API-consumers, API-providers en tooling.
Dat verschil wordt vooral belangrijk bij de combinatie van:
GET, POST en PUT, waarbij een JSON-payload de toestand van een resource
beschrijft;
PATCH met application/merge-patch+json, waarbij een JSON-payload alleen de
wijziging op een resource beschrijft.
Voor JSON Merge Patch is het onderscheid essentieel. RFC 7396 gebruikt drie
betekenissen:
- Property aanwezig met een waarde: overschrijf de bestaande waarde.
- Property aanwezig met
null: verwijder of leeg de bestaande waarde.
- Property afwezig: laat de bestaande waarde ongewijzigd.
Als een API in de GET-respons null-waarden weglaat, of het onderscheid
tussen null en afwezig niet consequent vastlegt in OpenAPI, kan een client
niet betrouwbaar bepalen welke PATCH nodig is. Dat vergroot de kans op
onbedoeld dataverlies en maakt automatische contractvalidatie lastiger.
2. Voorgestelde richtlijn
Leg in het API-contract expliciet vast of een property verplicht is, optioneel
is of expliciet null mag zijn. Hanteer daarbij voor JSON dezelfde betekenis in
alle operaties, en gebruik voor partiële updates altijd de interpretatie zoals in
application/merge-patch+json.
We onderscheiden twee soorten waarden:
| Soort waarde |
Betekenis |
Voorbeeld |
| Vaste waarde |
De property moet aanwezig zijn en een geldige waarde bevatten. |
naam: string |
| Optionele waarde |
De property mag expliciet geen waarde hebben. |
tweedeNaam: string | null |
3. Gedrag bij volledige JSON-payloads
Voor application/json bij GET, POST en PUT beschrijft de payload de
volledige toestand van de resource.
| Soort waarde |
Toestand in payload |
Interpretatie |
OpenAPI |
| Vaste waarde |
Afwezig |
Fout |
Opnemen in required |
| Vaste waarde |
null |
Fout |
Niet nullable |
| Optionele waarde |
Afwezig |
Interpreteren als null |
Niet opnemen in required; eventueel default |
| Optionele waarde |
null |
Geldige lege waarde |
Nullable |
| Optionele waarde |
Waarde |
Geldige waarde |
Nullable |
Richtlijn: stuur optionele waarden in responses bij voorkeur expliciet als
null terug. Daarmee blijft voor clients zichtbaar dat de property onderdeel is
van het contract, ook als er geen waarde is.
Bij requests betekent een afwezige optionele waarde dus dat de server geen waarde
voor die property vastlegt, alsof de client expliciet null had gestuurd.
In schema's voor volledige application/json-payloads kan default: null
eventueel als documentatie worden gebruikt. Laat de betekenis daar niet van
afhangen: default valideert niet en dwingt geen servergedrag af.
4. Gedrag bij merge-patch-payloads
Voor PATCH met application/merge-patch+json beschrijft de payload alleen de
wijziging. Een ontbrekende property betekent daarom iets anders dan in een
volledige resource-representatie.
| Soort waarde |
Toestand in payload |
Interpretatie |
OpenAPI |
| Vaste of optionele waarde |
Afwezig |
Geen wijziging |
Niet opnemen in required |
| Optionele waarde |
null |
Waarde verwijderen of leegmaken |
Nullable |
| Vaste waarde |
null |
Fout, bijvoorbeeld 400 Bad Request |
Niet nullable |
| Vaste of optionele waarde |
Waarde |
Waarde overschrijven |
Type volgens schema |
5. Voorbeeld
Een resource bevat een verplichte naam en een optionele tweedeNaam.
{
"naam": "Ada Lovelace",
"tweedeNaam": null
}
Een client die alleen naam wil aanpassen, stuurt:
PATCH /personen/123
Content-Type: application/merge-patch+json
{
"naam": "Augusta Ada Lovelace"
}
De afwezige property tweedeNaam blijft ongewijzigd.
Een client die tweedeNaam wil wissen, stuurt:
Een client mag naam niet wissen, omdat naam een vaste waarde is:
De server wijst die mutatie af.
OpenAPI-componenten hergebruiken
Gebruik bij voorkeur een gedeelde basis voor de properties en maak daarna per
payloadtype een eigen schema. Zet in de gedeelde basis wel de typen en
nullability, maar niet de required-lijst. Die lijst verschilt namelijk tussen
een volledige resource en een merge-patch.
components:
schemas:
PersoonVelden:
type: object
properties:
naam:
type: string
tweedeNaam:
type: string
nullable: true
Persoon:
description: Volledige resource voor GET, POST en PUT.
allOf:
- $ref: "#/components/schemas/PersoonVelden"
- type: object
required:
- naam
PersoonMergePatch:
description: Delta voor PATCH met application/merge-patch+json.
allOf:
- $ref: "#/components/schemas/PersoonVelden"
In dit voorbeeld gebruiken Persoon en PersoonMergePatch dezelfde
propertydefinities. Daardoor blijft naam overal een vaste waarde en blijft
tweedeNaam overal nullable. Het verschil zit alleen in aanwezigheid:
Persoon vereist naam, terwijl PersoonMergePatch geen enkele property
vereist omdat afwezig bij merge-patch "geen wijziging" betekent.
Gebruik dus wel allOf met een basis zoals PersoonVelden, waarin alleen de
gedeelde properties staan. Gebruik geen allOf waarbij PersoonMergePatch is
gebaseerd op het volledige Persoon-schema. required-velden uit een
allOf-basis blijven namelijk verplicht; OpenAPI biedt geen manier om die in
een afgeleid schema weer optioneel te maken.
Andersom, Persoon afleiden van PersoonMergePatch en daarna required
toevoegen, kan technisch wel. Toch is dat minder helder: een volledige resource
is geen specialisatie van een patch-operatie. Het koppelt de resource bovendien
aan patch-specifieke keuzes, zoals het ontbreken van defaults en de betekenis
van afwezige properties. Gebruik daarom liever een neutrale basiscomponent voor
de gedeelde velden, en leid zowel Persoon als PersoonMergePatch daarvan af.
Defaultwaarden
Gebruik default niet als mechanisme om het verschil tussen null en een
afwezige property te modelleren. Een OpenAPI-default is vooral documentatie voor
tooling en clients; het is geen validatieregel en geen garantie dat de server
die waarde invult. In een schema voor een volledige application/json-payload
kan default: null dus eventueel, maar alleen als toelichting. De semantiek
moet uit de richtlijn en de serverimplementatie volgen, niet uit de default.
Defaultwaarden mogen wel als er echt een business-default bestaat die de server
toepast wanneer een client de property weglaat. Leg die default dan vast op het
requestschema waarin de client de property mag weglaten. De property moet ook
onderdeel zijn van de volledige resource-representatie, zodat de server de
uiteindelijke waarde kan teruggeven. Bijvoorbeeld voor een property
communicatietaal die ook in de volledige Persoon-resource voorkomt:
components:
schemas:
PersoonAanmaken:
allOf:
- $ref: "#/components/schemas/PersoonVelden"
- type: object
properties:
communicatietaal:
type: string
enum:
- nl
- en
default: nl
required:
- naam
Gebruik default nooit in een merge-patch-schema om een afwezige property
alsnog een waarde te geven. Zet default: null ook niet in een gedeeld
basisschema dat door een merge-patch-schema via $ref of allOf wordt
hergebruikt. Bij application/merge-patch+json betekent afwezig altijd: geen
wijziging. Een default kan die semantiek in de weg zitten zodra tooling of
clientcode defaults materialiseert. Dan wordt een ontbrekende property alsnog
meegestuurd: met default: null als delete of unset, en met een andere default
als overschrijving.
6. Impact en compatibiliteit
| Wijziging in contract |
Impact op requests |
Impact op responses |
| Van vaste waarde naar optionele waarde |
Meestal compatibel: de server accepteert meer. |
Breaking change: clients kunnen opeens null ontvangen. |
| Van optionele waarde naar vaste waarde |
Breaking change: de server eist meer data. |
Meestal compatibel: clients ontvangen altijd een waarde. |
Deze richtlijn maakt het mogelijk om wijzigingen in nullability en aanwezigheid
van properties automatisch te toetsen als onderdeel van contractvalidatie.
Referenties
1. Probleem
In onze API-richtlijnen ontbreekt nu een eenduidige afspraak over het verschil
tussen een property die ontbreekt en een property met de waarde
null. Daardoorkan dezelfde payload op verschillende manieren worden geïnterpreteerd door
API-consumers, API-providers en tooling.
Dat verschil wordt vooral belangrijk bij de combinatie van:
GET,POSTenPUT, waarbij een JSON-payload de toestand van een resourcebeschrijft;
PATCHmetapplication/merge-patch+json, waarbij een JSON-payload alleen dewijziging op een resource beschrijft.
Voor JSON Merge Patch is het onderscheid essentieel. RFC 7396 gebruikt drie
betekenissen:
null: verwijder of leeg de bestaande waarde.Als een API in de
GET-responsnull-waarden weglaat, of het onderscheidtussen
nullen afwezig niet consequent vastlegt in OpenAPI, kan een clientniet betrouwbaar bepalen welke
PATCHnodig is. Dat vergroot de kans oponbedoeld dataverlies en maakt automatische contractvalidatie lastiger.
2. Voorgestelde richtlijn
Leg in het API-contract expliciet vast of een property verplicht is, optioneel
is of expliciet
nullmag zijn. Hanteer daarbij voor JSON dezelfde betekenis inalle operaties, en gebruik voor partiële updates altijd de interpretatie zoals in
application/merge-patch+json.We onderscheiden twee soorten waarden:
naam: stringtweedeNaam: string | null3. Gedrag bij volledige JSON-payloads
Voor
application/jsonbijGET,POSTenPUTbeschrijft de payload devolledige toestand van de resource.
requirednullnullrequired; eventueel defaultnullRichtlijn: stuur optionele waarden in responses bij voorkeur expliciet als
nullterug. Daarmee blijft voor clients zichtbaar dat de property onderdeel isvan het contract, ook als er geen waarde is.
Bij requests betekent een afwezige optionele waarde dus dat de server geen waarde
voor die property vastlegt, alsof de client expliciet
nullhad gestuurd.In schema's voor volledige
application/json-payloads kandefault: nulleventueel als documentatie worden gebruikt. Laat de betekenis daar niet van
afhangen:
defaultvalideert niet en dwingt geen servergedrag af.4. Gedrag bij merge-patch-payloads
Voor
PATCHmetapplication/merge-patch+jsonbeschrijft de payload alleen dewijziging. Een ontbrekende property betekent daarom iets anders dan in een
volledige resource-representatie.
requirednullnull400 Bad Request5. Voorbeeld
Een resource bevat een verplichte
naamen een optioneletweedeNaam.{ "naam": "Ada Lovelace", "tweedeNaam": null }Een client die alleen
naamwil aanpassen, stuurt:{ "naam": "Augusta Ada Lovelace" }De afwezige property
tweedeNaamblijft ongewijzigd.Een client die
tweedeNaamwil wissen, stuurt:{ "tweedeNaam": null }Een client mag
naamniet wissen, omdatnaameen vaste waarde is:{ "naam": null }De server wijst die mutatie af.
OpenAPI-componenten hergebruiken
Gebruik bij voorkeur een gedeelde basis voor de properties en maak daarna per
payloadtype een eigen schema. Zet in de gedeelde basis wel de typen en
nullability, maar niet de
required-lijst. Die lijst verschilt namelijk tusseneen volledige resource en een merge-patch.
In dit voorbeeld gebruiken
PersoonenPersoonMergePatchdezelfdepropertydefinities. Daardoor blijft
naamoveral een vaste waarde en blijfttweedeNaamoveral nullable. Het verschil zit alleen in aanwezigheid:Persoonvereistnaam, terwijlPersoonMergePatchgeen enkele propertyvereist omdat afwezig bij merge-patch "geen wijziging" betekent.
Gebruik dus wel
allOfmet een basis zoalsPersoonVelden, waarin alleen degedeelde properties staan. Gebruik geen
allOfwaarbijPersoonMergePatchisgebaseerd op het volledige
Persoon-schema.required-velden uit eenallOf-basis blijven namelijk verplicht; OpenAPI biedt geen manier om die ineen afgeleid schema weer optioneel te maken.
Andersom,
Persoonafleiden vanPersoonMergePatchen daarnarequiredtoevoegen, kan technisch wel. Toch is dat minder helder: een volledige resource
is geen specialisatie van een patch-operatie. Het koppelt de resource bovendien
aan patch-specifieke keuzes, zoals het ontbreken van defaults en de betekenis
van afwezige properties. Gebruik daarom liever een neutrale basiscomponent voor
de gedeelde velden, en leid zowel
PersoonalsPersoonMergePatchdaarvan af.Defaultwaarden
Gebruik
defaultniet als mechanisme om het verschil tussennullen eenafwezige property te modelleren. Een OpenAPI-default is vooral documentatie voor
tooling en clients; het is geen validatieregel en geen garantie dat de server
die waarde invult. In een schema voor een volledige
application/json-payloadkan
default: nulldus eventueel, maar alleen als toelichting. De semantiekmoet uit de richtlijn en de serverimplementatie volgen, niet uit de default.
Defaultwaarden mogen wel als er echt een business-default bestaat die de server
toepast wanneer een client de property weglaat. Leg die default dan vast op het
requestschema waarin de client de property mag weglaten. De property moet ook
onderdeel zijn van de volledige resource-representatie, zodat de server de
uiteindelijke waarde kan teruggeven. Bijvoorbeeld voor een property
communicatietaaldie ook in de volledigePersoon-resource voorkomt:Gebruik
defaultnooit in een merge-patch-schema om een afwezige propertyalsnog een waarde te geven. Zet
default: nullook niet in een gedeeldbasisschema dat door een merge-patch-schema via
$refofallOfwordthergebruikt. Bij
application/merge-patch+jsonbetekent afwezig altijd: geenwijziging. Een default kan die semantiek in de weg zitten zodra tooling of
clientcode defaults materialiseert. Dan wordt een ontbrekende property alsnog
meegestuurd: met
default: nullals delete of unset, en met een andere defaultals overschrijving.
6. Impact en compatibiliteit
nullontvangen.Deze richtlijn maakt het mogelijk om wijzigingen in nullability en aanwezigheid
van properties automatisch te toetsen als onderdeel van contractvalidatie.
Referenties