Skip to content

Eenduidige semantiek voor null en afwezige properties #341

Open

Description

@terborg

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:

  1. Property aanwezig met een waarde: overschrijf de bestaande waarde.
  2. Property aanwezig met null: verwijder of leeg de bestaande waarde.
  3. 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:

{
  "tweedeNaam": null
}

Een client mag naam niet wissen, omdat naam een 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 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

Activity

  1. TimvdLippe commented on Jul 1, 2026

    @TimvdLippe
    Contributor

    #345 vraagt om vergelijkbare verduidelijking voor zowel PUT als PATCH.

  2. BvanRaaij commented on Jul 16, 2026

    @BvanRaaij
    Contributor

    Ik ben erg voor de regel (zoals ook in de Zalando styleguide genoemd) die afdwingt dat null en absent properties op dezelfde manier behandeld worden. Wel twee opmerkingen:

    • ik denk dat de leesbaarheid van dit Issue beter wordt als je in plaats van termen als "vast waarde, optioneel. etc) de OpenAPI termen gebruikt die hier betrekking op hebben, nl required (dat aangeeft of de property in de payload aanwezig moet zijn) en nullable dat aangeeft of de waarde van de property null mag zijn.

    • Ik heb wel twijfel bij Merge patch in het algemeen er bestaat ook een JSON PATCH rfc6902 die weliswaar complexer maar ook eenduidiger is. RFC 7396 bevat verschillende voorbeelden waarbij het type van een waarde volledig kan veranderen, bijvoorbeeld van een object naar een string of van een array naar een scalaire waarde. Deze flexibiliteit kan lastig te hanteren zijn in sterk getypeerde talen zoals Java en C#, waar resourcemodellen doorgaans vaste eigenschapstypen hebben. Het ondersteunen van dergelijke gevallen kan extra logica of minder strikte typering vereisen.

    Om die reden lijkt JSON Merge Patch mij minder geschikt, tenzij we aanvullende beperkingen definiëren die dergelijke typewijzigingen voorkomen en het gedrag voorspelbaarder maken voor getypeerde implementaties.

    Een andere beperking is dat Merge Patch partiële collectie-operaties niet erg goed ondersteunt. Als de huidige resource bijvoorbeeld is:

    {
      "tags": ["a"]
    }

    en de client wil "b" toevoegen, dan moet het Merge Patch-document de volledige resulterende array bevatten:

    {
      "tags": ["a", "b"]
    }

    Dit vervangt de gehele tags-array in plaats van "voeg tag b toe" uit te drukken. In situaties met gelijktijdige toegang kan dit per ongeluk wijzigingen van een andere client overschrijven.

    Dit probleem kan worden verminderd door tags te modelleren als een aparte REST-resource, bijvoorbeeld door een /tags-endpoint toe te voegen voor het toevoegen of verwijderen van individuele tags. Dat vereist echter discipline in het API-ontwerp en het gebruik door clients. Als clients "tags" nog steeds via de hoofdresource bijwerken met Merge Patch, blijft het risico op onbedoeld overschrijven bestaan.

    Mijn voorkeur gaat dan ook uit naar RFC 6902, omdat dit de beoogde (partiële) update beter modelleert.

  3. terborg commented on Jul 21, 2026

    @terborg
    Author

    Bedankt voor de feedback. Op zich zou ik het voorstel nog graag los willen zien van een specifieke PATCH-variant, of een brede discussie over PATCH (en idempotency).

    Het gaat mij om de semantiek van required en nullable in de API-specificatie. Wanneer de definities van required (moet het veld aanwezig zijn) en nullable (mag de waarde null zijn) niet scherp zijn vastgelegd, ontstaat er onduidelijkheid over het verschil tussen een veld niet aanpassen en een veld leegmaken.

    Wanneer we deze regels in de OpenAPI-specificaties eenduidig vastleggen, is de betekenis van wat er over the wire gaat altijd helder. Dat JSON Merge Patch of JSON Patch daardoor ook voorspelbaarder werken, is een nuttig gevolg hiervan.

    Mijn voorstel is daarom om in de design rules de focus te leggen op het verduidelijken van de afspraken rondom required, nullable en default.

  4. BvanRaaij commented on Jul 21, 2026

    @BvanRaaij
    Contributor

    Helemaal mee eens Rutger. Ik zag dat je zelf ook naar de Zalando Styleguide verwees waar deze regel ook in staat. En de een duidelijke regel van de interpretatie van absent en null (nl dat dat hetzelfde geïnterpreteerd moet worden) is sowieso waardevol en is zeker niet alleen van toepassing bij gebruik van de PATCH methode:.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions