# How to reuse schemas and responses with components and $ref

> Move every repeated schema, parameter, and response under components and reference it with $ref, so generators emit one named type instead of a copy per use.

Source: https://voxgig.com/howto/reuse-schemas-with-components-and-ref

- Audience: api-producer
- Level: beginner
- Verified: 2026-10-17
- Published: 2026-10-17

## Short answer

Put every schema, parameter, header, response, and request body that appears twice under `components`, give it a name you would give a class, and point each use at it with `$ref`. Generators turn component keys into type names, so an inline schema becomes an anonymous type per use. In OpenAPI 3.1 a `$ref` may carry a `summary` and `description`, and a Schema Object `$ref` may sit beside other keywords; in 3.0 anything beside `$ref` is ignored.

---
## You will need

An OpenAPI 3.1 document with at least one repeated schema, and Node 22 or later for the generator
and the linter that show the difference. Verified 2026-10-17 against Node 22.22.0,
`openapi-typescript` 7.13.0, `@stoplight/spectral-cli` 6.16.3, TypeScript 5.9.3, and `yaml`
2.8.2, installed from npm in the sample directory. The rules come from the
[Components Object](https://spec.openapis.org/oas/v3.1.0.html#components-object) and
[Reference Object](https://spec.openapis.org/oas/v3.1.0.html#reference-object) sections of
OpenAPI 3.1.0, and from the [3.0.3 text](https://spec.openapis.org/oas/v3.0.3.html#reference-object)
where the two differ.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [Components across files with relative `$ref`](https://spec.openapis.org/oas/v3.1.0.html#relative-references-in-uris) | Several teams own different resources and review by file | A bundling step for tools that read one file, and a generator that may name the type after the file rather than the component | One team edits one file and merge conflicts are rare |
| [Components in one document](https://spec.openapis.org/oas/v3.1.0.html#components-object) | Most documents: one file every tool reads, with every repeated piece named once | Names you must choose well, because every generator turns the key into a type name, and a components section that grows with the API | A description too large for one file to review |
| [Inline schemas](https://spec.openapis.org/oas/v3.1.0.html#schema-object) | A throwaway document, or a shape used exactly once | An anonymous type per use in every generated client, and the same object rendered once per operation in the docs | Anything a client will be generated from more than once |

Inline is quick to write and expensive to generate from: every use is a type of its own, named by
the generator after the operation or left anonymous. One document with a components section costs
you naming, since the key becomes the type name in every language you generate. Splitting across
files buys review and ownership at the price of a bundling step and, in the generator used here, a
type named after the file.

## Name the schema once and reference it

The document below describes the same three operations as an inline copy of it, with every
repeated piece moved under `components` and referenced from where it is used.

```yaml title="components.yaml"
  /meters/{id}:
    get:
      operationId: get_meter
      description: One meter by id.
      tags: [meters]
      parameters:
        - $ref: '#/components/parameters/MeterId'
      responses:
        '200':
          description: One meter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Meter'
                description: The meter that was asked for. A sibling of $ref, which 3.1 allows.
        '404':
          $ref: '#/components/responses/NotFound'
components:
  schemas:
    Meter:
      type: object
      description: A metering point.
      required: [id, serial]
      properties:
        id: { type: string }
        serial: { type: string }
        installed_at: { type: string, format: date-time }
        owner:
          $ref: '#/components/schemas/Party'
          description: Who pays for this meter. A sibling of $ref, which 3.1 allows.
    Party:
      type: object
      description: A customer or a landlord.
      properties:
        name: { type: string }
```

`types.mjs` runs [`openapi-typescript`](https://openapi-ts.dev/introduction) over the inline copy,
this document, and a third copy whose schemas live in their own files. It counts two things: the
names under `components.schemas` in the output, and how often the meter is spelled out as an
object literal inside the operations.

```bash
node types.mjs
```

```text output
inline.yaml
  named schema types                none
  meter literals in the operations  3
components.yaml
  named schema types                Meter, Party, MeterInput, Problem
  meter literals in the operations  0
split/openapi.yaml
  named schema types                Meter, Problem, meter, problem
  meter literals in the operations  0
components.yaml, a description beside a $ref
  on a property      kept, as a JSDoc comment
  on a media type    dropped
```

The inline document produces no named types and three copies of the meter, one per operation. The
components document produces four names and no copies; every use is `components["schemas"]["Meter"]`.
OpenAPI Generator makes the same distinction with names rather than literals. Its documentation
says inline schemas become separate schemas whose auto-generated name "may not look good to
everyone," and gives `meta_200_response` as its own example.[^1] The component key is the name
you would have had to pick anyway, so pick it as you would name a class.

The split tree is read without a bundle, and the generator hoists each file under its filename:
`meter`, with `Meter` kept as an alias and both operations typed against `meter`. The name you
chose survives as that alias, but the one the client shows is the file's.

## Reuse parameters, responses, headers, and request bodies the same way

The [Components Object](https://spec.openapis.org/oas/v3.1.0.html#components-object) has ten
fixed fields in 3.1: `schemas`, `responses`, `parameters`, `examples`, `requestBodies`, `headers`,
`securitySchemes`, `links`, `callbacks`, and `pathItems`. Each is a map from a name to the object
or to a `$ref`, and every key must match `^[a-zA-Z0-9\.\-_]+$`.[^2]

```yaml title="components.yaml"
  parameters:
    MeterId:
      name: id
      in: path
      required: true
      description: The meter id.
      schema: { type: string }
  headers:
    RateLimitRemaining:
      description: Requests left in the current window.
      schema: { type: integer }
  requestBodies:
    MeterInput:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MeterInput'
  responses:
    NotFound:
      description: No meter has that id.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unprocessable:
      description: The body did not validate.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
```

```bash
node print-types.mjs components.yaml components
```

```text output
    responses: {
        /** @description No meter has that id. */
        NotFound: {
            headers: {
                [name: string]: unknown;
            };
            content: {
                "application/problem+json": components["schemas"]["Problem"];
            };
        };
        /** @description The body did not validate. */
        Unprocessable: {
            headers: {
                [name: string]: unknown;
            };
            content: {
                "application/problem+json": components["schemas"]["Problem"];
            };
        };
    };
    parameters: {
        /** @description The meter id. */
        MeterId: string;
    };
    requestBodies: {
        MeterInput: {
            content: {
                "application/json": components["schemas"]["MeterInput"];
            };
        };
    };
    headers: {
        /** @description Requests left in the current window. */
        RateLimitRemaining: number;
    };
```

Every key is a name in the output, and both error responses point at one `Problem`. A client
that maps `application/problem+json` to a form, or an SDK that throws a typed error, is written
once against `Problem` rather than once per operation.

## Put a description beside the $ref only in 3.1

The two versions disagree about what may sit next to `$ref`. In 3.0.3 the Reference Object has one
fixed field, `$ref`, and the specification says that any properties added to it SHALL be ignored,
in capitals. In 3.1.0 the [Reference Object](https://spec.openapis.org/oas/v3.1.0.html#reference-object)
gains `summary` and `description`, which by default override the referenced component's. The
Schema Object is JSON Schema 2020-12, whose `$ref` is an applicator that other keywords can appear
alongside.[^3] The same document under Spectral, once as 3.0.3 and once as 3.1.0:

```bash
npx spectral lint sibling-3.0.yaml
```

```text output
 31:30  error  no-$ref-siblings  $ref must not be placed next to any other properties  paths./meters/{id}.get.responses[200].content.application/json.schema.description
 41:24  error  no-$ref-siblings  $ref must not be placed next to any other properties  components.schemas.Meter.properties.owner.description

✖ 2 problems (2 errors, 0 warnings, 0 infos, 0 hints)
```

```bash
npx spectral lint sibling-3.1.yaml
```

```text output
No results with a severity of 'error' found!
```

The Spectral rule [`no-$ref-siblings`](https://docs.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules)
exists because 3.0 tooling drops the sibling, and the rule turns itself off for 3.1. The
generator run earlier shows that the permission is not the same as support. In the 3.1 document it
kept the description on the `owner` property as a doc comment and dropped the one on the media
type schema.[^4] Write the sibling where your tools read it, and keep the referenced component's
own `description` as the one that is always rendered.

## Check it worked

The test that guards the design generates types from both documents and asserts the names and
the counts the comparison promised.

```js title="refs.test.mjs"
test('components become named types; inline schemas become one literal per use', async () => {
  const withComponents = await generate('components.yaml')
  assert.deepEqual(schemaNames(withComponents), ['Meter', 'Party', 'MeterInput', 'Problem'])
  assert.equal(meterLiteralsInOperations(withComponents), 0)
  const inline = await generate('inline.yaml')
  assert.deepEqual(schemaNames(inline), [])
  assert.equal(meterLiteralsInOperations(inline), 3)
})
```

```bash
node --test refs.test.mjs
```

```text output
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6281.578862
```

Six tests pass. The others walk every `$ref` in the document and assert that each resolves, and
check every component key against the pattern. Two more pin the split tree's file-named types and
run Spectral over the 3.0 and 3.1 documents and the two documents in the next section.

## When it goes wrong

A path item kept under `components.pathItems` and referenced from `paths` is valid 3.1: the
[Path Item Object](https://spec.openapis.org/oas/v3.1.0.html#path-item-object) allows a `$ref` to
a referenced definition. The generator reads it, but the linter rejects it.

```yaml title="pathitems.yaml"
paths:
  /meters/{id}:
    $ref: '#/components/pathItems/MeterById'
```

```bash
node print-types.mjs pathitems.yaml paths
```

```text output
export interface paths {
    "/meters/{id}": components["pathItems"]["MeterById"];
}
```

```bash
npx spectral lint pathitems.yaml
```

```text output
 14:16  error  oas3-schema  "~1meters~1{id}" property must not have unevaluated properties.  paths./meters/{id}

✖ 1 problem (1 error, 0 warnings, 0 infos, 0 hints)
```

The Spectral rule `oas3-schema` reads the referenced path item as an unexpected property. Until
every tool in your pipeline agrees, write path items in place under `paths` and reuse their parts,
which is what `components.yaml` does.

A component key with a space fails validation, and the message names the pattern.

```bash
npx spectral lint badkey.yaml
```

```text output
 29:11  error  oas3-schema  "schemas" property must match pattern "^[a-zA-Z0-9._-]+$".  components.schemas
 29:11  error  oas3-schema  "schemas" property property name must be valid.             components.schemas

✖ 2 problems (2 errors, 0 warnings, 0 infos, 0 hints)
```

`Meter Reading` cannot be a key, and neither can a name with a slash or a colon. Use
`MeterReading`, which is also what the generated type will be called.

A generated type is named after a file. The schemas were split into files and the generator was
pointed at the tree, as the split run showed. Bundle by reference first, which
[split and bundle a large OpenAPI document](/howto/split-and-bundle-an-openapi-document) covers,
so the generator sees one document with your component names.

## When not to do this

Do not move a schema used once into `components` to be tidy. A component is a name you have
promised to every generated client, and removing or renaming one is a breaking change to clients
that never asked for it.

Do not name a component after the operation that returns it. `GetMeterResponse` is what a
generator invents when you gave it nothing, and a document that invents the same thing by hand has
spent the one advantage of naming.

Do not put a `description` beside a `$ref` in a document that declares `openapi: 3.0.x`. The
specification says it is ignored, Spectral says so, and most tools drop the text you wrote.

Do not reference path items from `paths` while a linter that rejects them guards your pipeline.
The feature is in the specification, but the tool run here refuses it, so the green build is worth
more than the reuse.

Do not split a document across files before a second team edits it. The split costs a bundling
step, and this generator a type name, for conflict-free editing a single editor never needed.

## Related how-tos

- [Split and bundle a large OpenAPI document](/howto/split-and-bundle-an-openapi-document)

- [Model a polymorphic response with a discriminator](/howto/polymorphic-response-with-a-discriminator)

- [Write operationIds that generate clean SDK method names](/howto/operationids-that-generate-clean-sdk-method-names)

- [Publish an OpenAPI 3.0 copy beside your 3.1 document](/howto/publish-openapi-3-0-alongside-3-1)

## Last verified

Verified 2026-10-17 against Node 22.22.0, `openapi-typescript` 7.13.0, `@stoplight/spectral-cli`
6.16.3, TypeScript 5.9.3, and `yaml` 2.8.2. Every output block is what the command preceding it
printed. OpenAPI Generator was not run; its naming is quoted from its documentation. Redoc and
Scalar were not run, so the rendering claims are what their documentation and the generated types
imply rather than screenshots.

[^1]: The OpenAPI Generator page on
[inline schema naming](https://openapi-generator.tech/docs/customization/#inline-schema-naming)
says that inline schemas become separate schemas automatically and that the auto-generated name
"may not look good to everyone," in its own words. It then shows
`meta_200_response` and `inline_object_2` being renamed with `--inline-schema-name-mappings`. A tool that documents its
own names as a problem and ships a flag for them has described the problem precisely.

[^2]: The key pattern `^[a-zA-Z0-9\.\-_]+$` appears word for word in
[3.1.0](https://spec.openapis.org/oas/v3.1.0.html#components-object) and in 3.0.3. It escapes the
dot, which needs no escaping inside a character class, and the hyphen, which does where it stands.
Without its backslash, that hyphen would make a range from the dot to the underscore, taking in
the slash and the colon. The pattern admits a name made entirely of dots. It shuts out the space, the slash, and
the tilde, so a key goes into a `$ref` fragment as written, with no
[JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901.html) escape and no percent-encoding.

[^3]: JSON Schema's core specification states the permission as an aside. Section
[8.2.3.1](https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1) defines `$ref`
as an applicator whose results are the results of the referenced schema, and then adds, in a note,
that this definition means other keywords can appear alongside it. The rule that changed a decade
of tooling behavior is a consequence noted in passing.

[^4]: The Spectral
[rule documentation](https://github.com/stoplightio/spectral/blob/develop/docs/reference/openapi-rules.md)
gives the history in two sentences. Before 3.1, keywords next to `$ref` "were ignored by most
tooling, but not all," and the result was "inconsistent experiences depending on what combinations
of tools are used," in its words. The generator on this page keeps one sibling and drops another in
a 3.1 document, so the inconsistency has survived the rule that was written about it.