Skip to main content
Adopt

IPA-124: Repeated Fields

Providing clients with lists of data can be complex. Aligning on a single strategy for providing clients with repeated fields allows the complexity to be reduced when clients need to modify the lists.

Guidance

  1. Repeated fields must use a plural field name.

    components:
    schemas:
    Project:
    type: object
    properties:
    tags:
    type: array
    items:
    type: string
    Why:

    A plural name signals at a glance that the field holds many values, so a reader expects an array before reading the type.

    components:
    schemas:
    Project:
    type: object
    properties:
    tag:
    type: array
    items:
    type: string
    Why:

    A singular name on an array field is misleading: the name says one value while the type says many. Naming and cardinality should agree.

    1. For each schema under components.schemas, list every property whose type is array, including arrays nested inside objects and array items.

    2. For each array property, inspect the field name and decide whether it is grammatically plural (tags, members, items) rather than singular (tag, member, item).

    3. Report any array property whose name is singular as a violation.

  2. Repeated fields should have an enforced upper bound that will not cause a single resource payload to become too large. A good rule of thumb is 100 elements.

    components:
    schemas:
    Project:
    type: object
    properties:
    members:
    type: array
    maxItems: 100
    items:
    type: string
    Why:

    maxItems caps the array, so a single resource payload cannot grow without limit no matter how many members are added.

    components:
    schemas:
    Project:
    type: object
    properties:
    members:
    type: array
    items:
    type: string
    Why:

    With no maxItems, the array is unbounded and the resource payload can grow arbitrarily large as members accumulate.

  3. If repeated data has the chance of being too large, the API should use a sub-resource instead.

    paths:
    /projects/{projectId}/members:
    get:
    operationId: listMembers
    responses:
    "200":
    description: A page of members.
    Why:

    A potentially large list is exposed as its own collection, so it can be paged through rather than embedded whole in the parent resource payload.

    components:
    schemas:
    Project:
    type: object
    properties:
    members:
    type: array
    maxItems: 100000
    items:
    type: string
    Why:

    The data is expected to grow large, yet it is embedded as an inline array with a huge bound. Every read of the parent resource carries the entire list, which a sub-resource collection would avoid.

    1. For each array property in components.schemas, estimate the realistic maximum element count from the field name, description, and any documented limits.

    2. Decide whether the data can grow large enough that embedding the whole list in every parent payload is a concern (for example, an upper bound well above the 100-element rule of thumb, or growth driven by end-user activity).

    3. For such a property, check whether the list is instead exposed as a paged sub-resource collection under the parent path.

    4. Report any list that can grow large but is embedded inline rather than modeled as a sub-resource.

  4. Client-owned repeated fields must be respected by the server.

    components:
    schemas:
    Project:
    type: object
    properties:
    labels:
    type: array
    items:
    type: string
    description: Client-owned. Stored and returned exactly as supplied.
    Why:

    The field is client-owned, and the server stores and returns the array as supplied, so a read returns the same list a write sent.

    components:
    schemas:
    Project:
    type: object
    properties:
    labels:
    type: array
    items:
    type: string
    description: Client-owned. The server sorts and de-dupes it.
    Why:

    The field is client-owned, but the server quietly reorders and de-duplicates it, so the value read back differs from the value written and a declarative client sees drift.

    1. Identify the repeated fields that are client-owned per IPA-111: those not documented as server-owned or readOnly.

    2. For each, inspect the implementing source or run the create and read operations to observe what the server stores and returns.

    3. Confirm a written list is returned with the same elements, in the same order, with duplicates preserved.

    4. Report any client-owned repeated field that the server silently alters on write or read.

  5. The server must not modify the order of elements or remove duplicates, unless the field is explicitly declared as a set (see List vs Set below).

    components:
    schemas:
    Project:
    type: object
    properties:
    roles:
    type: array
    x-xgen-array-semantic: set
    items:
    type: string
    Why:

    The field is declared a set with x-xgen-array-semantic, so reordering and de-duplication are part of its contract. A consumer knows order is not significant and treats equality without regard to it.

    components:
    schemas:
    Project:
    type: object
    properties:
    roles:
    type: array
    items:
    type: string
    description: A list of roles.
    Why:

    Absent x-xgen-array-semantic: set, the array is a list, so the server must preserve order and duplicates. If it reorders or de-duplicates the value at runtime, a declarative client sees drift it cannot anticipate.

    1. For each repeated field, send a write with a known element order that includes a deliberate duplicate.

    2. Read the resource back and compare the returned array to the written one for reordering or removed duplicates.

    3. When the list is transformed, confirm the field declares x-xgen-array-semantic: set.

    4. Report any repeated field the server reorders or de-duplicates that is not declared as a set.

List vs Set

Arrays fall into two kinds, and the distinction governs whether order and duplicates are part of the resource state. Classifying each array — and declaring sets explicitly — lets clients and downstream tooling reason correctly about order and equality.

  1. Every array property must be classified as either a list or a set. Set-like arrays must be declared with the x-xgen-array-semantic extension (see IPA-131); when the extension is absent, the array is treated as a list.

    • List — order is meaningful and elements may repeat. This is the default when the semantic is not declared.
    • Set — order is not meaningful and elements are unique.
    components:
    schemas:
    Project:
    type: object
    properties:
    environments:
    type: array
    items:
    type: string
    roles:
    type: array
    x-xgen-array-semantic: set
    items:
    type: string
    Why:

    environments is left as a list because order matters, while roles declares x-xgen-array-semantic: set because it is an unordered, duplicate-free collection. Each array's intent is explicit.

    components:
    schemas:
    Project:
    type: object
    properties:
    roles:
    type: array
    items:
    type: string
    description: An unordered, duplicate-free set of roles.
    Why:

    The description says the field is an unordered set, but it carries no x-xgen-array-semantic: set declaration, so tooling treats it as a list and expects order and duplicates to be preserved.

    1. For each array property, determine whether element order is part of the resource state.

    2. Confirm arrays whose order is not meaningful declare x-xgen-array-semantic: set.

    3. Flag any array described as unordered or duplicate-free that omits the set declaration.

  2. For all lists, the server must return elements in a deterministic, stable order across repeated GET responses for the same resource state

    • For client-owned lists, the order must be the one the client provided, per the client-owned fields rule above.
    • Unstable ordering causes false drift in declarative clients.
    components:
    schemas:
    Project:
    type: object
    properties:
    members:
    type: array
    items:
    type: string
    description:
    Server-owned list. Returned in a stable order across reads.
    Why:

    Repeated reads of the same resource state return members in the same order, so a declarative client never sees the list change without an actual change to the resource.

    components:
    schemas:
    Project:
    type: object
    properties:
    members:
    type: array
    items:
    type: string
    description: Server-owned list. Returned in arbitrary order per read.
    Why:

    The server returns members in a different order on each read of an unchanged resource, so a declarative client repeatedly detects and tries to reconcile drift that does not exist.

    1. For each list field, read the same unchanged resource multiple times.

    2. Compare element order across the responses.
    3. Report any list whose order varies across reads of the same resource state.

  3. For arrays declared as sets, the server must not return duplicate elements

    • The server may return elements in any order; the order in a response is not part of the resource state.
    • Clients must treat equality without regard to order.
    components:
    schemas:
    Project:
    type: object
    properties:
    roles:
    type: array
    x-xgen-array-semantic: set
    items:
    type: string
    description:
    Set. Returned without duplicates; order is not significant.
    Why:

    The set is returned with unique elements, and consumers compare two responses by membership rather than position, so a reordered response is not treated as a change.

    components:
    schemas:
    Project:
    type: object
    properties:
    roles:
    type: array
    x-xgen-array-semantic: set
    items:
    type: string
    description: Set that may contain repeated elements.
    Why:

    The field is declared a set but the server returns duplicate elements, so its contents contradict the set contract and consumers cannot rely on membership semantics.

    1. For each field declared x-xgen-array-semantic: set, read the resource and inspect the returned array.

    2. Confirm the array contains no duplicate elements.

    3. Report any set field whose response includes duplicates.

Update Strategy

  1. A resource may use one of two strategies to enable updating a repeated field: direct update using the standard Update method, which can only update the entire list; or custom Add and Remove methods.

  2. When choosing a custom method approach, the field must not be settable via Create or Update operations.

    components:
    schemas:
    Project:
    type: object
    properties:
    members:
    type: array
    readOnly: true
    items:
    type: string
    Why:

    The repeated field is managed only through the custom add and remove methods, so it is marked readOnly and the create and update request bodies cannot set it.

    components:
    schemas:
    Project:
    type: object
    properties:
    members:
    type: array
    items:
    type: string
    Why:

    Custom add and remove methods manage the list, yet the field is also writable through create and update, leaving two competing ways to set it and an ambiguous source of truth.

    1. Determine which repeated fields are managed by custom Add and Remove methods by scanning for custom-method operations that target a list.

    2. For each such field, locate its property in the create and update request body schemas.

    3. Confirm the field is readOnly or absent from those request bodies, so the standard create and update operations cannot set it.

    4. Report any custom-method-managed repeated field that remains settable through create or update.

  3. Declarative-friendly resources must use the standard Update method, and not introduce Add and Remove methods. To learn more, see IPA-127.

    paths:
    /projects/{projectId}:
    put:
    operationId: updateProject
    requestBody:
    content:
    application/json:
    schema:
    type: object
    properties:
    members:
    type: array
    items:
    type: string
    Why:

    A declarative-friendly resource updates the whole list through the standard update method, so the desired state is expressed in one place that a declarative client can reconcile.

    paths:
    /projects/{projectId}:members:add:
    post:
    operationId: addProjectMembers
    /projects/{projectId}:members:remove:
    post:
    operationId: removeProjectMembers
    Why:

    A declarative-friendly resource exposes imperative add and remove methods, which express list changes as deltas a declarative client cannot reconcile against a desired state.

    1. Identify the resources documented as declarative-friendly per IPA-127.

    2. For each, list the operations that modify its repeated fields.

    3. Confirm those fields are updated through the standard update method, and that no custom Add or Remove methods exist for them.

    4. Report any declarative-friendly resource that exposes Add or Remove methods for a repeated field.