Skip to content

Add human-readable title to error oneOf branches in OpenAPI output #1580

Description

@ChromeGG

Summary

When a procedure defines multiple errors that share the same HTTP status, OpenAPIGenerator emits the response body as an anonymous oneOf of objects. Because none of the branches carry a title, Swagger UI (and other doc renderers) display the union as (object | object | object), which is hard to read and gives no hint about which error is which.

It would be great if oRPC labelled each error branch out of the box - e.g. with the error code as the schema title - so tooling renders something like (PENGUIN_NAME_BANNED | PENGUIN_COLONY_FULL | UnexpectedError).

Reproduction

Define two errors with the same status on a procedure:

const createPenguin = oc
  .route({ method: 'POST', path: '/' })
  .input(createPenguinRequestDtoSchema)
  .output(penguinResponseDtoSchema)
  .errors({ PENGUIN_NAME_BANNED, PENGUIN_COLONY_FULL }); // both status: 400

export const PENGUIN_NAME_BANNED = {
  status: 400,
  message: "You can't create a penguin with this name",
  data: z.object({
    reason: z.enum(['name_is_banned', 'name_reserved']),
    attemptedName: z.string(),
  }),
};

export const PENGUIN_COLONY_FULL = {
  status: 400,
  message: 'The colony has reached its maximum capacity',
  data: z.object({
    limit: z.number().int().positive(),
    current: z.number().int().nonnegative(),
  }),
};

Generate the OpenAPI doc:

const generator = new OpenAPIGenerator({
  schemaConverters: [new ZodToJsonSchemaConverter()],
});
const doc = await generator.generate(contract, { /* ... */ });

Current output

"400":
  content:
    application/json:
      schema:
        oneOf:
          - type: object
            properties:
              defined: { const: true }
              code: { const: PENGUIN_NAME_BANNED }
              status: { const: 400 }
              message: { type: string }
              data: { ... }
            # no title
          - type: object
            properties:
              code: { const: PENGUIN_COLONY_FULL }
              # ...
            # no title
          - type: object   # undefined-error fallback
            properties:
              defined: { const: false }
              code: { type: string }
              # no title

Swagger UI renders this as (object | object | object):

Image

Expected / desired output

Each branch carries a title, so renderers can label the union:

oneOf:
  - title: PENGUIN_NAME_BANNED
    type: object
    # ...
  - title: PENGUIN_COLONY_FULL
    type: object
    # ...
  - title: UnexpectedError   # the defined: false fallback branch
    type: object
    # ...

Rendered as (PENGUIN_NAME_BANNED | PENGUIN_COLONY_FULL | UnexpectedError).

Proposed behaviour

For each error branch in the generated oneOf:

  • Use the branch's code const as the schema title.
  • For the generated undefined-error fallback branch (defined: false, non-const code), use a stable name like UnexpectedError.
  • Optionally, emit an OpenAPI discriminator keyed on code for the defined branches (the fallback has no const code, so it can't be part of a strict discriminator mapping — that's why a plain title is the safer default).

A config flag (e.g. errorBranchTitles: true or a customizable titleFor(code) callback) would be a nice escape hatch.

Current workaround

I post-process the generated document and inject titles onto every error oneOf branch:

function titleErrorUnionBranches(node: unknown): void {
  if (Array.isArray(node)) {
    node.forEach(titleErrorUnionBranches);
    return;
  }
  if (!node || typeof node !== 'object') return;

  const schema = node as Record<string, unknown>;
  if (Array.isArray(schema.oneOf)) {
    for (const branch of schema.oneOf) {
      if (!branch || typeof branch !== 'object' || 'title' in branch) continue;
      const props = (branch as any).properties ?? {};
      const codeConst = props.code?.const;
      if (typeof codeConst === 'string') (branch as any).title = codeConst;
      else if (props.defined?.const === false) (branch as any).title = 'UnexpectedError';
    }
  }
  Object.values(schema).forEach(titleErrorUnionBranches);
}

This works, but it would be much nicer to have it built in.

Environment

  • @orpc/openapi, @orpc/contract, @orpc/server, @orpc/zod: 1.14.3
  • Zod 4 (ZodToJsonSchemaConverter from @orpc/zod/zod4)

Additional information

  • Would you be willing to help implement this feature?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions