Structuring OpenAPI YAML: components, Reuse, Naming and Versioning
AI generated
{ }
GET
OpenAPI · YAML · REST API Design · Documentation
Structuring OpenAPI YAML:
components, Reuse, Naming and Versioning

An unstructured OpenAPI specification turns into a maintenance burden the moment more than three developers work on it. With components/schemas, consistent $ref references and a clear versioning strategy, the API description stays the single source of truth for documentation, code generation and contract tests.

15 min read components · $ref · oneOf · versioning · naming OpenAPI 3.1 · Swagger · Symfony API Platform

1. Why structure matters so much in OpenAPI

An OpenAPI specification is not a static document, it is the contract between API producer and API consumer. When backend developers, frontend developers and integration partners all work from the same YAML file, structure decides whether changes are communicated cleanly or whether silent breaking changes creep in. A poorly structured OpenAPI YAML with duplicated schema definitions, inconsistent field names and no versioning becomes a maintenance burden the moment a first API version has to be replaced by a second.

The decisive difference between a well structured and a chaotic OpenAPI file is not the completeness of the endpoint documentation, it is the reusability of definitions. A schema like ProductResponse that is defined inline in eight different endpoints has to be adjusted eight times whenever a field changes, with the realistic consequence that some spots get forgotten. With $ref: '#/components/schemas/ProductResponse' the definition exists exactly once, and every endpoint inherits the change automatically.

OpenAPI 3.1 brought substantial improvements over 3.0: full JSON Schema Draft 2020-12 compatibility, better support for null types via type: [string, 'null'], and the ability to define webhooks as first-class citizens. Anyone starting a new API specification today should go straight to 3.1 to benefit from these improvements.

2. components/schemas: the central building blocks

The components section in OpenAPI 3.x is where all reusable definitions live: schemas, parameters, request bodies, response definitions, security schemes and more. For structuring schemas one clear rule applies: any schema that appears more than once belongs in components/schemas. That also applies to schemas used only once but with a distinct domain meaning of their own, such as Money, Address or ContactPerson.

Within components/schemas it is worth grouping by domain context rather than by technical function. Instead of choosing CreateRequestBody and UpdateRequestBody as the base structure, it is better to structure by the domain object: Product, ProductCreate, ProductUpdate, ProductResponse. This distinction becomes important as soon as code generators like openapi-generator or Spectral linting rules work on the specification, consistent naming leads to usable generated code, while inconsistent names lead to cryptic class names.


# openapi.yaml - components/schemas section
openapi: "3.1.0"
info:
  title: Product API
  version: "2.0.0"
  description: |
    Product catalog API with full CRUD support.
    Breaking changes are communicated via changelog.

components:
  schemas:
    # ---- Shared primitives ----
    Money:
      type: object
      required: [amount, currency]
      properties:
        amount:
          type: integer
          description: Amount in smallest currency unit (cents)
          example: 1999
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          example: EUR

    # ---- Domain objects ----
    ProductBase:
      type: object
      required: [name, sku]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          example: "Leather Handbag Classic"
        sku:
          type: string
          pattern: '^[A-Z0-9\-]{3,64}$'
          example: "LH-CLASSIC-001"
        price:
          $ref: '#/components/schemas/Money'

    ProductCreate:
      allOf:
        - $ref: '#/components/schemas/ProductBase'
        - type: object
          required: [category_id]
          properties:
            category_id:
              type: integer
              example: 42

    ProductResponse:
      allOf:
        - $ref: '#/components/schemas/ProductBase'
        - type: object
          required: [id, created_at]
          properties:
            id:
              type: integer
              readOnly: true
              example: 1001
            created_at:
              type: string
              format: date-time
              readOnly: true

3. $ref reuse: eliminating duplicates systematically

$ref is the most powerful tool in OpenAPI, yet in practice it is often used too sparingly. A well structured specification should use $ref references exclusively for every endpoint, no inline schema definitions except for trivial one-liners. This applies not only to request and response bodies but also to parameters, security requirements and response definitions. The components/responses section, for example, can hold standardized error responses such as NotFound, Unauthorized and UnprocessableEntity, which are then wired into every endpoint via $ref.

A common mistake when using $ref: the description override. When a $ref object carries its own description, many tools (especially Swagger UI and older openapi-generator versions) ignore the local description and show the description defined in the target schema instead. OpenAPI 3.1 improved this behavior with the new summary keyword and clearer override rules. Anyone using tools with 3.0 support should test local overrides before relying on the behavior.

4. Naming conventions for schemas, paths and parameters

Consistent naming is the foundation of a maintainable OpenAPI specification. For schema names, PascalCase has become the standard: ProductResponse, OrderCreateRequest, PaymentMethod. Paths follow the REST standard with plural lowercase nouns and hyphens: /products, /order-items, /payment-methods. Parameter names inside schemas use snake_case: created_at, product_id, unit_price. These three conventions apply regardless of which language the backend uses, the API convention is decoupled from the implementation.

Operation IDs deserve particular care, since code generators derive function names from them. The convention verb_resource_qualifier in camelCase is widely used: getProduct, listProducts, createProduct, updateProduct, deleteProduct. A poor operation ID like api_products_post results in unreadable generated code. Spectral linting rules can check operation ID conventions automatically and enforce them in CI pipelines.


# Consistent naming in paths and operations
paths:
  /products:
    get:
      operationId: listProducts
      summary: List all products
      tags: [Products]
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Paginated product list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      operationId: createProduct
      summary: Create a new product
      tags: [Products]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductCreate'
      responses:
        '201':
          description: Product created successfully
          headers:
            Location:
              schema:
                type: string
                format: uri
              description: URL of the created product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductResponse'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

components:
  parameters:
    PageParam:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPageParam:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

5. Using oneOf, allOf and anyOf deliberately

allOf, oneOf and anyOf are composition tools in OpenAPI that are frequently misused. allOf is the right tool for inheritance and schema extension: a ProductUpdate schema inherits all fields from ProductBase via allOf and adds update-specific fields. oneOf, on the other hand, models polymorphism, schemas that must be exactly one of several possible types, a classic use case being different payment methods: CreditCardPayment, PayPalPayment, BankTransferPayment. The discriminator (discriminator.propertyName) helps code generators and validators identify the correct schema.

anyOf allows a match with one or more schemas at the same time and is rarely used correctly. The common mistake is confusing it with oneOf: anyOf is semantically looser and allows overlap between schema variants, which makes validation logic more complex. In most API designs either allOf (for extension) or oneOf (for alternatives) is the better choice. When a field can optionally be null, the cleanest solution in OpenAPI 3.1 is type: [string, 'null'], no oneOf with a null schema is needed anymore.

6. Versioning strategies: URL, header, content type

API versioning is one of the most frequently discussed questions in REST API design. The three established strategies are URL versioning (/v1/products, /v2/products), header versioning (API-Version: 2) and content type negotiation (Accept: application/vnd.mironsoft.v2+json). Each strategy has different implications for the documentation structure in an OpenAPI specification.

URL versioning is the most pragmatic solution and the standard in most public APIs. Implementing it in OpenAPI is straightforward: either a separate YAML file per version or different server entries with a version prefix. The boundary between versions is immediately visible to developers, browser caching works without extra configuration, and the OpenAPI specification stays simply structured. The downside: clients have to migrate explicitly to new versions, and old versions have to be run in parallel.

Header versioning keeps URLs stable and is favored by API Platform and some large hyperscalers. In OpenAPI it is modeled as an optional header parameter in the components/parameters section. The downside is worse visibility: without specific tooling it is not immediately obvious which version a request targets. For internal APIs between controlled services, header versioning can nonetheless be the cleaner solution.

7. Multiple YAML files: $ref to external documents

Once an OpenAPI specification grows past 500 lines, splitting it across multiple files pays off. OpenAPI supports external $ref references: $ref: './schemas/product.yaml#/ProductResponse'. The cleanest directory structure separates by domain context: schemas/ for all data models, parameters/ for reusable parameters, responses/ for standard response definitions, and paths/ for endpoint definitions. The main file openapi.yaml then contains only metadata, security definitions and $ref pointers to the sub files.

Tools like redocly bundle or swagger-cli bundle can merge these distributed files into a single file for deployment and code generation. During development you work with the split files, for the build step it gets bundled. Spectral linting works on both variants. Anyone using API Platform with Symfony can generate the specification directly from PHP attributes and only needs to maintain manually the parts the framework does not cover automatically.


# Error responses as reusable components
components:
  responses:
    Unauthorized:
      description: Authentication credentials missing or invalid
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'
          example:
            type: "https://mironsoft.de/errors/unauthorized"
            title: "Unauthorized"
            status: 401
            detail: "Bearer token missing or expired"

    NotFound:
      description: Requested resource does not exist
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

    UnprocessableEntity:
      description: Validation failed
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ValidationProblemDetail'

  schemas:
    ProblemDetail:
      type: object
      description: RFC 7807 Problem Details for HTTP APIs
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
          format: uri

    ValidationProblemDetail:
      allOf:
        - $ref: '#/components/schemas/ProblemDetail'
        - type: object
          properties:
            violations:
              type: array
              items:
                type: object
                required: [field, message]
                properties:
                  field:
                    type: string
                  message:
                    type: string

8. Defining error responses consistently

Consistent error responses are a sign of professional API design. The standard RFC 7807 "Problem Details for HTTP APIs" defines a unified JSON format for error messages with the fields type, title, status, detail and the optional instance. The content type is application/problem+json. When every error follows this structure, API clients can implement a single error handling routine for all endpoints instead of writing their own parsing logic per endpoint.

In the OpenAPI specification that means: a schema ProblemDetail in components/schemas, a collection of standard responses in components/responses, and consistent $ref usage in every single endpoint. Validation errors (422 Unprocessable Entity) extend ProblemDetail via allOf with a violations array that holds the field name and the error message for each invalid field. This structure maps directly onto Symfony Validator exceptions and is implemented by default in API Platform.

9. Comparing structuring approaches

There are substantial differences in maintainability, tool compatibility and development speed between an OpenAPI specification that grew ad hoc and one structured from the start.

Aspect Inline definitions (messy) components + $ref (recommended) Gain
Schema change Manually in N places Once in components No inconsistencies
Code generation Duplicated classes Clean DTO types Usable generated code
Linting (Spectral) Many warnings Clean lint CI integration possible
Error responses Different per endpoint RFC 7807 unified Uniform client logic
Versioning Breaking changes unclear Changelog + deprecation Safe migration

The investment in structure pays off by the second feature: whoever uses $ref consistently on the first endpoint saves the time otherwise spent copying and adapting schema definitions on the second. Linting tools like Spectral detect missing $ref usage and can be used as a quality gate in CI pipelines.

Mironsoft

REST API design, OpenAPI specifications and Symfony API Platform

Want an OpenAPI specification that works as a single source of truth?

We structure your OpenAPI YAML with components, $ref reuse and consistent naming conventions, so code generation, linting and contract tests can build on the specification reliably.

Specification review

Spectral linting, structure audit and recommendations for $ref migration

API design consulting

Naming conventions, versioning strategy and schema modeling

Symfony integration

API Platform with OpenAPI, automatic specification generation and tests

10. Summary

A professionally structured OpenAPI YAML file uses components/schemas as its central building blocks and consistently eliminates duplicates through $ref references. PascalCase for schemas, snake_case for fields and camelCase for operation IDs create consistent naming that gives code generators usable output. allOf models inheritance, oneOf models polymorphism. Error responses follow RFC 7807 and are defined once in components/responses. The versioning strategy is settled from the start, before the first breaking change becomes unavoidable.

The biggest lever is automation: Spectral linting in the CI pipeline, code generation from the specification, and contract tests with schemas that validate directly from the YAML file. An OpenAPI specification that only serves as documentation gives away three quarters of its potential. The specification should be exactly what its name promises: a machine-readable contract used by tooling at every level.

Structuring OpenAPI YAML: the essentials at a glance

components/schemas

Every reused schema belongs in components. $ref eliminates duplicates and makes changes effective in a single place.

Naming conventions

PascalCase for schemas, snake_case for fields, camelCase for operation IDs. Consistency is a prerequisite for usable generated code.

Error responses

RFC 7807 ProblemDetail as a unified error format. Define it once in components/responses, reference it everywhere via $ref.

Versioning

URL versioning (/v1/, /v2/) for public APIs. Separate YAML files or server entries per version. Maintain a changelog and deprecation headers.

11. FAQ: Structuring OpenAPI YAML

1When to move a schema into components?
Whenever it is reused or has a distinct domain meaning of its own. Always move out Money, Address, ContactPerson, keep inline only for trivial one-liners.
2allOf vs oneOf vs anyOf?
allOf for inheritance (all must match), oneOf for polymorphism (exactly one), anyOf for loose alternatives (one or more). Most of the time you only need allOf and oneOf.
3Versioning strategy for public APIs?
URL versioning (/v1/, /v2/) is standard: visible, cacheable, simple OpenAPI structure. Header versioning for internal services.
4Nullable fields in OpenAPI 3.1?
type: [string, 'null'] directly. In 3.0 it was still nullable: true. The oneOf-null pattern from 3.0 is obsolete in 3.1.
5What is RFC 7807?
Standard for HTTP error messages: type, title, status, detail as JSON with content type application/problem+json. One error structure for all endpoints.
6External $ref to other YAML files?
Yes: $ref: './schemas/product.yaml#/ProductResponse'. redocly bundle or swagger-cli bundle merge all files together for deployment.
7Check naming conventions automatically?
Spectral linting with a custom ruleset in CI. Checks operation IDs, schema names, required fields like description automatically on every commit.
8Define a discriminator for oneOf?
discriminator.propertyName + discriminator.mapping. The discriminator field must be required and constant in every sub schema.
9Tools for OpenAPI validation?
Spectral (linting), swagger-cli (syntax), redocly (bundle + preview), Schemathesis (contract tests). All integrate into GitHub Actions / GitLab CI.
10Breaking changes in a new API version?
Deploy the new version under /v2/, run /v1/ in parallel for at least 6 months. Deprecation headers in v1, provide a changelog and migration docs.