Skip to main content
Adopt

IPA-125: Single Type in Request and Response

API requests and responses should strive for clarity by representing each field with a single, well-defined type. This helps maintain consistency and reduces ambiguity for API consumers. This convention also ensures that downstream tooling is well-supported and prevents issues for IaC tools as it simplifies state management.

Guidance

Splitting Fields for Multiple Value Types

  1. API producers should not use oneOf with base types like integer or string when the field can hold multiple distinct value types.

    components:
    schemas:
    Setting:
    type: object
    properties:
    enabled:
    type: boolean
    threshold:
    type: integer
    Why:

    Each value type lives in its own typed field, so a consumer reads enabled as a boolean and threshold as an integer without inspecting the payload to find out which type arrived.

    components:
    schemas:
    Setting:
    type: object
    properties:
    value:
    oneOf:
    - type: boolean
    - type: integer
    - type: string
    Why:

    A single field that may arrive as a boolean, an integer, or a string forces every consumer to branch on the runtime type, and generated clients cannot give the field one stable static type.

  2. API producers should split such fields into separate, clearly named fields with appropriate types.

    components:
    schemas:
    Index:
    type: object
    properties:
    isArray:
    type: boolean
    arrayObjects:
    type: array
    items:
    type: object
    singleObject:
    type: object
    Why:

    The three value shapes become three named fields with explicit types, so the name of each field documents what it holds and the type system enforces it.

    components:
    schemas:
    Index:
    type: object
    properties:
    index:
    oneOf:
    - type: boolean
    - type: array
    items:
    type: object
    - type: object
    Why:

    One overloaded field carries a boolean, an array, or an object depending on the case, so the field name describes none of them and a consumer cannot tell from the schema which shape to send.

    1. For each schema under $.components.schemas, list every property whose definition uses oneOf.

    2. For each such oneOf, determine whether the members are distinct base types (boolean, integer, number, string) or different structural shapes (object versus array versus scalar).

    3. When the members are distinct value types rather than variants of one typed object, treat the single field as overloaded.

    4. Report each overloaded field, noting that the fix is one named, single-typed field per value shape.

Fields Containing Multiple Object Types

  1. API producers may use fields that contain multiple objects when request and response objects allow explicitly setting the type of the object.

  2. In OpenAPI each oneOf property must be accompanied by a discriminator property that defines when each exact type is used.

    components:
    schemas:
    Notification:
    oneOf:
    - $ref: "#/components/schemas/EmailNotification"
    - $ref: "#/components/schemas/SmsNotification"
    discriminator:
    propertyName: channel
    mapping:
    email: "#/components/schemas/EmailNotification"
    sms: "#/components/schemas/SmsNotification"
    Why:

    The discriminator names the field (channel) that selects a variant and maps each value to one schema, so a consumer resolves the concrete type from the payload without guessing.

    components:
    schemas:
    Notification:
    oneOf:
    - $ref: "#/components/schemas/EmailNotification"
    - $ref: "#/components/schemas/SmsNotification"
    Why:

    Without a discriminator, nothing in the schema indicates which variant a given payload represents, so the concrete type must be inferred by trial validation against each branch.

  3. In OpenAPI each discriminator property must be accompanied by a oneOf, anyOf, or allOf property (OAS 3.1.0 4.8.25.1).

    components:
    schemas:
    Payment:
    oneOf:
    - $ref: "#/components/schemas/CardPayment"
    - $ref: "#/components/schemas/BankPayment"
    discriminator:
    propertyName: method
    Why:

    The discriminator sits beside a oneOf composition, so it has a set of candidate schemas to select among.

    components:
    schemas:
    Payment:
    type: object
    properties:
    method:
    type: string
    discriminator:
    propertyName: method
    Why:

    A discriminator with no oneOf, anyOf, or allOf sibling has no candidate schemas to choose between, so the selection it describes points at nothing.

  4. If multiple oneOf models define a property with the same name, that property must have the same data type in each model.

    components:
    schemas:
    Event:
    oneOf:
    - $ref: "#/components/schemas/CreatedEvent"
    - $ref: "#/components/schemas/DeletedEvent"
    CreatedEvent:
    type: object
    properties:
    id:
    type: string
    DeletedEvent:
    type: object
    properties:
    id:
    type: string
    Why:

    Both variants type the shared id field as a string, so a consumer reads id the same way regardless of which variant arrives.

    components:
    schemas:
    Event:
    oneOf:
    - $ref: "#/components/schemas/CreatedEvent"
    - $ref: "#/components/schemas/DeletedEvent"
    CreatedEvent:
    type: object
    properties:
    id:
    type: string
    DeletedEvent:
    type: object
    properties:
    id:
    type: integer
    Why:

    The id field is a string in one variant and an integer in the other, so the type of a field with one name depends on which variant arrived, defeating the single-type goal.