Skip to main content
Adopt

IPA-111: Server-Modified Values and Defaults

Server-modified and default values often make it harder to implement state-driven clients. These clients are often unable to tell when their desired state matches the current state for these fields, as the rules by which a server may modify and return values are complex, not public, and not repeatable.

Guidance

Single Owner Fields

  1. Fields must have a single owner, whether that is the client or the server.

    • Server-owned fields are fields that are controlled and modified by the service
    • Client-owned fields are fields that are controlled by the client
    components:
    schemas:
    Cluster:
    type: object
    properties:
    clusterName:
    type: string
    stateName:
    type: string
    readOnly: true
    Why:

    clusterName is client-owned and mutable, while stateName is server-owned and marked readOnly: true. Each field has a single, unambiguous owner.

    1. For each field in the schema, determine whether the client or the server controls its value.

    2. Confirm fields are not jointly controlled — a field modified by both the client and the server has no single owner.

    3. Flag fields whose ownership is ambiguous or undocumented.

  2. Server-owned fields must be documented as read-only

    components:
    schemas:
    Cluster:
    type: object
    properties:
    stateName:
    type: string
    description: Current state of the cluster, set by the service.
    readOnly: true
    Why:

    stateName is controlled by the service and is documented as read-only, signaling to clients that they cannot set it.

    1. Identify the server-owned fields in the schema.
    2. Confirm each is documented as read-only.
    3. Flag any server-owned field not documented as read-only.

  3. In OpenAPI, server-owned fields must have readOnly: true

    components:
    schemas:
    Cluster:
    type: object
    properties:
    stateName:
    type: string
    readOnly: true
    Why:

    The server-owned stateName field sets readOnly: true, the OpenAPI mechanism for marking a field as not settable by the client.

  4. Server-owned fields must not be accepted in request bodies for Create or Update operations

    components:
    schemas:
    Cluster:
    type: object
    properties:
    clusterName:
    type: string
    stateName:
    type: string
    readOnly: true
    Why:

    stateName is marked readOnly: true, so it is excluded from Create and Update request bodies and the service ignores any client-supplied value.

    1. For each Create or Update operation, inspect the request body schema.

    2. Confirm no server-owned (read-only) field is accepted in the request body.

    3. Flag operations that accept a server-owned field in their request body.

  5. Server-owned fields may be omitted if appropriate to the API

    • API Producers should document whether the server-owned field is guaranteed, or if it can be omitted in the response
  6. All fields that are not explicitly documented as server-owned must be considered client-owned

    components:
    schemas:
    Cluster:
    type: object
    properties:
    clusterName:
    type: string
    stateName:
    type: string
    readOnly: true
    Why:

    stateName is explicitly documented as server-owned via readOnly: true; clusterName carries no such marking and is therefore client-owned.

    1. For each field, check whether it is explicitly documented as server-owned.

    2. Treat every field without that documentation as client-owned.

    3. Flag fields whose intended client ownership conflicts with how the service actually treats them.

  7. The server must respect the value (or lack thereof) of all client-owned fields and not modify them

    components:
    schemas:
    Cluster:
    type: object
    properties:
    clusterName:
    type: string
    Why:

    clusterName is client-owned, so the service stores and returns whatever the client provides without modifying it.

    1. Set a client-owned field to a known value via a Create or Update operation.

    2. Retrieve the resource and inspect the field.
    3. Confirm the service did not modify the value the client sent. Flag any client-owned field the server alters.

  8. The server must always return the same value the client sent (or absence of value) for client-owned fields

    components:
    schemas:
    Cluster:
    type: object
    properties:
    clusterName:
    type: string
    Why:

    For the client-owned clusterName, the service echoes back exactly the value the client sent, or omits it if the client sent no value.

    1. Send a client-owned field with a specific value, and separately send a request omitting it.

    2. Retrieve the resource after each request.
    3. Confirm the response returns the same value the client sent, and the same absence when no value was sent. Flag discrepancies.

note

For resources where all fields are server-owned, see read-only resources and read-only singleton resources.

Optional Fields with Server Defaults

Assigning a server-side default in place of a client-omitted value during resource creation is an anti-pattern: it creates hybrid ownership where the field is client-owned but the server effectively chooses a value. State-driven clients and downstream tooling then cannot tell whether a returned value was client-set or server-filled, and they report drift on every reconciliation.

  1. Optional request fields must be returned by the server with the same value (or absence of value) the client provided

    components:
    schemas:
    Backup:
    type: object
    properties:
    retentionDays:
    type: integer
    description:
    Optional. Returned exactly as provided; absent when not set.
    Why:

    When the client omits retentionDays, the server leaves it absent rather than filling in a default, so a read reflects exactly what the client set and no drift appears.

    components:
    schemas:
    Backup:
    type: object
    properties:
    retentionDays:
    type: integer
    description: Optional. Defaults to 30 when omitted by the client.
    Why:

    The client omits retentionDays, but the server returns 30. The field is client-owned, yet the response carries a server-chosen value, so a declarative client sees a difference it never set and reconciles against it indefinitely.

    1. Identify optional request fields that are client-owned per Single Owner Fields.

    2. Create a resource omitting each such field, then read it back.

    3. Confirm the field is returned as the client left it — absent when omitted — rather than populated with a server-chosen default.

    4. Report any optional client-owned field the server fills with a default on the response.

  2. Legacy fields that cannot be remediated away from the optional-plus-server-default pattern must be annotated per IPA-131 so declarative tooling can reconcile the hybrid ownership

    • New APIs must not rely on this escape hatch; it exists only for legacy fields that cannot change.
    • Optional boolean fields are exempt and must not be annotated — they are governed by Boolean Values.
    components:
    schemas:
    Backup:
    type: object
    properties:
    retentionDays:
    type: integer
    x-xgen-server-computed-when-client-omitted: true
    description: Legacy. Server assigns a value when the client omits it.
    Why:

    The legacy field keeps its server-default behavior but declares x-xgen-server-computed-when-client-omitted, so tooling knows the returned value may be server-filled and does not treat it as client drift.

    components:
    schemas:
    Backup:
    type: object
    properties:
    retentionDays:
    type: integer
    description: Legacy. Server assigns a value when the client omits it.
    Why:

    The field retains the optional-plus-server-default behavior but carries no annotation, so declarative tooling sees the server-assigned value as client-owned and reports spurious drift.

    1. Identify optional client-owned fields for which the server still assigns a value when the client omits one.

    2. Confirm each such field that cannot be remediated declares x-xgen-server-computed-when-client-omitted: true.

    3. Confirm no boolean field carries the annotation.

    4. Report any unannotated legacy field exhibiting the pattern, and any new (non-legacy) field relying on it.

Effective Values

There are instances where a service will allocate, generate, or calculate a value that may differ from what the client specified.

  1. An attribute with an effective value must be expressed as two fields in the API:

    • A client-owned mutable field that may be optionally set by the user and must not be modified by the service
    • A server-owned read-only field that records the effective value decided on by the service
    components:
    schemas:
    Cluster:
    type: object
    properties:
    instanceSize:
    type: string
    effectiveInstanceSize:
    type: string
    readOnly: true
    Why:

    The desired instanceSize is a client-owned mutable field, while effectiveInstanceSize is a server-owned read-only field recording the value the service decided on.

    1. Identify attributes whose effective value may differ from what the client specified.

    2. Confirm each is expressed as two fields: a client-owned mutable field and a server-owned read-only field.

    3. Flag attributes that collapse the desired and effective values into a single field.

  2. Effective values must be named by prefixing effective to the mutable field's name

    components:
    schemas:
    Cluster:
    type: object
    properties:
    instanceSize:
    type: string
    effectiveInstanceSize:
    type: string
    readOnly: true
    Why:

    The effective field is named effectiveInstanceSize, prefixing effective to the mutable field name instanceSize.

  3. In OpenAPI, the effective value field must have readOnly: true

    components:
    schemas:
    Cluster:
    type: object
    properties:
    effectiveInstanceSize:
    type: string
    readOnly: true
    Why:

    The effectiveInstanceSize field sets readOnly: true, marking the server-decided effective value as not settable by the client.

Example

A cluster's instance size may have a different computed value from the server if auto-scaling is enabled. The client specifies their desired instance size, but the server may scale it up or down based on load.

// For managing a cluster with auto-scaling enabled
{
"instanceSize": "M10",
"effectiveInstanceSize": "M30"
}

Sensitive Fields

Some resource fields carry secrets — passwords, API keys, private credentials. The API design must prevent accidental exposure of these values in responses.

  1. Sensitive fields must be marked in OpenAPI so that consumers and tooling can identify them (see IPA-117 for the required annotations).

    components:
    schemas:
    ApiKey:
    type: object
    properties:
    displayName:
    type: string
    key:
    type: string
    format: password
    writeOnly: true
    Why:

    The key property carries format: password and writeOnly: true, so tooling and reviewers know the field contains a secret and must not appear in responses.

    components:
    schemas:
    ApiKey:
    type: object
    properties:
    displayName:
    type: string
    key:
    type: string
    Why:

    The key property lacks any annotation — no format: password, no writeOnly — so neither consumers nor tooling can tell that the field carries a secret.

    1. Identify fields that carry secrets — passwords, API keys, credentials, or other sensitive material.

    2. For each sensitive field, confirm it carries an OpenAPI annotation (e.g. format: password, writeOnly: true) that marks it as sensitive.

    3. Report sensitive fields that have no marking or rely only on naming conventions.

    Depends on

Sensitive fields must follow one of the following information-flow patterns:

  1. Write-only. The client submits the value on Create or Update and the server must not return it in any response.

    components:
    schemas:
    ApiKeyCreate:
    type: object
    properties:
    displayName:
    type: string
    key:
    type: string
    format: password
    writeOnly: true
    Why:

    The key is write-only — the client sends it on create, and the server never returns it in subsequent Get or List responses.

    components:
    schemas:
    UserUpdate:
    type: object
    properties:
    email:
    type: string
    password:
    type: string
    format: password
    writeOnly: true
    Why:

    When updating a user, the client submits the new password on Update. It is writeOnly: true, so the server accepts it but never returns it in any response.

    components:
    schemas:
    ApiKey:
    type: object
    properties:
    displayName:
    type: string
    key:
    type: string
    Why:

    The key is not marked write-only, so the client expects it to appear in responses — leaking the secret.

    1. Identify fields whose value is submitted by the client and must not be returned.

    2. Confirm each such field is marked writeOnly: true and does not appear in response schemas.

    3. Flag any write-only field that appears in a response schema.

  2. Create-response-only. For values the client cannot retrieve later (e.g. server-generated API keys), the field may appear in the Create response so the client can capture it, but must not appear in subsequent Get or List responses. Create-response-only is the sanctioned exception to the schema consistency rule.

    components:
    schemas:
    # Create (POST) response: returns the secret once
    ApiKeyCreateResponse:
    type: object
    properties:
    id:
    type: string
    displayName:
    type: string
    key:
    type: string
    format: password
    readOnly: true
    # Get and List response: omits the secret
    ApiKey:
    type: object
    properties:
    id:
    type: string
    displayName:
    type: string
    Why:

    The server-generated key appears only in the Create response schema, marked readOnly since the client never sets it. The Get and List schema omits it entirely, so the secret cannot be retrieved later.

    components:
    schemas:
    # Single schema reused by Create, Get, and List
    ApiKey:
    type: object
    properties:
    id:
    type: string
    displayName:
    type: string
    key:
    type: string
    format: password
    readOnly: true
    Why:

    A single response schema carries key across Create, Get, and List, so the secret is returned on every read instead of only once on creation.

    1. Identify fields that are server-generated and shown only once (e.g., initial API key secret).

    2. Confirm the field appears only in the Create response schema and is absent from the Get and List response schemas.

    3. Flag any create-response-only field that appears in Get or List response schemas.

When a masked representation of a sensitive field (e.g. **** or a last-4 display value) needs to appear in Get or List responses, it must be modeled as a separate read-only redacted sibling property following the Effective Values pattern. The raw and redacted values must not share the same property name. A redacted sibling may be added to either a write-only or a create-response-only field independently of the chosen information-flow pattern.

  1. A masked display of a sensitive field (e.g. **** or a last-4 display value) must be modeled as a separate read-only redacted sibling property, not reusing the raw field's name.

    components:
    schemas:
    ApiKey:
    type: object
    properties:
    key:
    type: string
    format: password
    writeOnly: true
    keyRedacted:
    type: string
    description: Last four characters of the API key.
    readOnly: true
    Why:

    The raw value is key (write-only); the masked display is keyRedacted (read-only, last-4 display). They are distinct properties with different names, so no ambiguity exists about which carries the secret.

    components:
    schemas:
    ApiKey:
    type: object
    properties:
    key:
    type: string
    Why:

    A single property named key cannot simultaneously be write-only and carry a masked display. Consumers cannot tell whether the value is the raw secret or a redacted version.

    1. Find fields whose response value is a masked or truncated display of a secret (e.g. ****, last-4).

    2. Confirm the masked value lives on a separate property with a distinct name (e.g. keyRedacted) rather than replacing the raw field.

    3. Report any field that mixes a raw secret and a masked display under the same property name.

Boolean Values

  1. Optional boolean fields must default to false

    • Many serialization systems won’t distinguish false values from unset values which introduces complications when a boolean defaults to true
    • This avoids a tri-state (true / false / unset) that is hard for both producers and consumers to reason about, so optional booleans must not carry the server-default annotation used for other optional fields
    components:
    schemas:
    ClusterDescriptionProcessArgs:
    type: object
    properties:
    javascriptDisabled:
    type: boolean
    default: false
    Why:

    The boolean is modeled as javascriptDisabled defaulting to false, so an unset value and a false value mean the same thing and no user intervention is required for the default.

    components:
    schemas:
    ClusterDescriptionProcessArgs:
    type: object
    properties:
    javascriptEnabled:
    type: boolean
    default: true
    Why:

    javascriptEnabled defaults to true, but many serialization systems cannot distinguish false from unset, forcing users to always set the value explicitly.

    1. Identify the default value of each boolean field.

    2. Confirm the default is false, renaming the field if necessary so the false default carries the intended meaning.

    3. Flag boolean fields that default to true.

Example

package example

// Bad
// Given the go struct
type ClusterDescriptionProcessArgs struct {
JavascriptEnabled bool `json:"javascriptEnabled"`
}
// the following call will default to false and requires user intervention to set
// JavascriptEnabled to true
sdk.UpdateClusterAdvancedConfiguration(new(ClusterDescriptionProcessArgs))
// Users need to always set the true value even if the API defaults to true
sdk.UpdateClusterAdvancedConfiguration(&ClusterDescriptionProcessArgs{JavascriptEnabled: true}))

// Good
type ClusterDescriptionProcessArgs struct {
JavascriptDisabled bool `json:"javascriptEnabled"`
}
// the following call will default to false and requires user intervention to set
// JavascriptDisabled to true
sdk.UpdateClusterAdvancedConfiguration(new(ClusterDescriptionProcessArgs))