IPA-130: Dynamic Content Fields
Resource properties that hold a dynamic JSON value — configuration objects, filters, metadata, templated payloads — should be modeled as a dynamic object type, so that clients and tooling can work with the content natively rather than as opaque text. Modeling such a property as a string containing the serialized JSON instead pushes serialization concerns onto clients and invites round-trip drift, because servers routinely normalize the JSON (whitespace, key ordering, reformatting of nested structures). Declarative clients then interpret the representation-level difference as state drift.
Guidance
Properties whose value is JSON must be modeled as a JSON object in requests and responses — never as a string holding the serialized form.
components:
schemas:
Webhook:
type: object
properties:
url:
type: string
filters:
type: object
additionalProperties: trueWhy:filtersis a real JSON object on the wire. A client sets keys and reads them back as fields, and the server can normalize whitespace or key order without changing what the client sees.components:
schemas:
Webhook:
type: object
properties:
url:
type: string
filters:
type: stringWhy:filtersis a JSON string. The client has to serialize it before sending and parse it after reading. If the server reformats the JSON, the returned string differs byte-for-byte from what was sent, and a declarative client reads that as drift.Collect every property of a schema, including those nested in objects and in array items.
Keep only the properties typed as
stringwith noformat. Set the rest aside.Check the name of each one. Names like
config,filters,metadata,payload, orsettingshint that the value is really JSON.Check the description. Wording such as "JSON object", "serialized JSON", or "escaped JSON string" means the value is JSON.
Check any example. A value written as a quoted object, like
"{...}", means the value is JSON.Before flagging, rule out plain text that merely contains braces: free-form notes, a query string, a code snippet, a regex. These stay as
string.Flag each property that holds JSON but is typed as
string. The fix is to change itstypetoobject.
When the JSON has a well-defined schema, the API must model that schema in the resource definition. Clients, documentation, and tooling benefit from knowing the shape of the content.
components:
schemas:
Webhook:
type: object
properties:
url:
type: string
filters:
type: object
properties:
event:
type: string
enum: [CREATED, UPDATED, DELETED]
minSeverity:
type: integer
required: [event]Why:The shape of
filtersis known, so it is spelled out:eventis an enum,minSeverityis an integer, andeventis required. Clients get validation and typed fields, and the generated docs show what a filter looks like.components:
schemas:
Webhook:
type: object
properties:
url:
type: string
filters:
type: object
additionalProperties: trueWhy:The filter shape is fixed and known, but the schema models it as a free-form object. Nothing tells a client that
eventexists, which values are valid, or that it is required. The known structure is thrown away and pushed back onto every consumer.Collect the properties that hold JSON under
$.components.schemas(nested objects and array items included): everyobject-typed property, plus anystring-typed property flagged byIPA-130-must-model-dynamic-json.Set aside the ones whose shape is already written out — an
objectwith apropertieslist and types. Those already comply.What remains is modeled loosely: a bare
object,additionalPropertiesonly, or astring. The real shape is not in the spec for these.Find the real shape outside the spec: read the request handler or validation code, the internal docs, or a few real request and response payloads.
Decide whether that shape is fixed. It is fixed (known) when the same keys appear every time and the server validates them. It is dynamic when the keys are arbitrary and client-chosen.
If the shape is dynamic, stop — this guideline does not apply (see
IPA-130-should-model-dynamic-schema).If the shape is known, flag the property. The fix is to spell out its
properties, give each atype, and addenum,format, orrequiredwhere they apply.
Depends onWhen the JSON is genuinely dynamic (arbitrary client-provided keys and values), the property should be modeled as an object with dynamic keys.
components:
schemas:
Document:
type: object
properties:
metadata:
type: object
additionalProperties:
type: stringWhy:Clients pick the keys, so the schema declares dynamic keys with
additionalPropertiesand a typed value. The shape stays JSON and the server has no reason to reserialize it.components:
schemas:
Document:
type: object
properties:
metadata:
type: object
properties:
owner:
type: string
team:
type: stringWhy:metadataholds arbitrary client-chosen keys, but the schema pins it to a closedpropertieslist. No key outsideownerandteamcan be declared, so the dynamic map the field is meant to hold can't be expressed.additionalPropertiesis what opens the key set. (Ametadata: stringwould also be wrong, but that isIPA-130-must-model-dynamic-json's concern.)Collect every
object-typed property under$.components.schemas, including nested objects and array items.Keep the ones whose keys are arbitrary or client-chosen. Judge from the name, description, or examples — labels, metadata, config maps, and tags are typical.
If one of these is still typed as
string, fix that underIPA-130-must-model-dynamic-jsonfirst; it then arrives here as anobject.Look at how each one declares its keys.
additionalProperties(optionally with a typed value) allows open keys; a closedpropertieslist does not.Flag any of these properties that use a closed
propertieslist or otherwise lackadditionalProperties. The fix is to declare the keys withadditionalProperties.
Depends on