GuidesConceptsExtract Inline Schemas

Extract inline schemas

We have experimental support for “extracting inline schemas” behind the --extract-inline-schemas / OPENAPI_EXTRACT_INLINE_SCHEMAS=true configuration flag.

What does this mean?

There are basically two ways you can define schemas in your openapi specifications:

  • Named schemas
  • Inline schemas

These are handled differently by code generation. Enabling --extract-inline-schemas aims to make inline schemas emit similar code to named schemas.

Named schema example

Normally when writing openapi specifications it is desirable to make use of $ref and define your schemas as named components.

paths:
  /list/{listId}:
    parameters:
      - $ref: '#/components/parameters/listId'
    get:
      operationId: getTodoListById
      responses:
        200:
          description: 'success'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoList'
components:
  schemas:
    TodoList:
      type: object
      required:
        - id
        - name
        - totalItemCount
        - incompleteItemCount
        - created
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        totalItemCount:
          type: number
        incompleteItemCount:
          type: number
        created:
          type: string
          format: date-time

When we run code generation for this, we expect a type and a schema for the TodoList to be generated, something like:

import {z} from 'zod'
 
export type t_TodoList = {
  id: string
  name: string
  totalItemCount: number
  incompleteItemCount: number
  created: string
}
 
export const s_TodoList = z.object({
  id: z.string(),
  name: z.string(),
  totalItemCount: z.coerce.number(),
  incompleteItemCount: z.coerce.number(),
  created: z.string().datetime({ offset: true }),
})

This is useful, as it means that we can easily reference the type, or use the schema as we require.

Inline Schema Example

However, not everyone will write their specifications using named $refs, and instead inline schemas may be used. This is especially prolific when generating the specification from implementation code in our experience.

Consider the same example as above, but with the schema inlined:

paths:
  /list/{listId}:
    parameters:
      - $ref: '#/components/parameters/listId'
    get:
      operationId: getTodoListById
      responses:
        200:
          description: 'success'
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - name
                  - totalItemCount
                  - incompleteItemCount
                  - created
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  totalItemCount:
                    type: number
                  incompleteItemCount:
                    type: number
                  created:
                    type: string
                    format: date-time
components:
  schemas: {}

By default, this will be emitted as in-line types / schemas

export type GetTodoListById = (
  params: Params<t_GetTodoListByIdParamSchema, void, void>,
  respond: GetTodoListByIdResponder,
  ctx: RouterContext,
) => Promise<
  | KoaRuntimeResponse<unknown>
  | Response<
      200,
      {
        id: string
        name: string
        totalItemCount: number
        incompleteItemCount: number
        created: string
      }
    >
  | Response<StatusCode4xx, t_Error>
  | Response<StatusCode, void>
>
 
const getTodoListByIdResponseValidator = responseValidationFactory(
  [
    [
      "200",
      z.object({
        id: z.string(),
        name: z.string(),
        totalItemCount: z.coerce.number(),
        incompleteItemCount: z.coerce.number(),
        created: z.string().datetime({ offset: true }),
      }),
    ],
    ["4XX", s_Error],
  ],
  z.undefined(),
)
 
router.get("getTodoListById", "/list/:listId", async (ctx, next) => {
  // ...
 
  const responder = {
    with200() {
      return new KoaRuntimeResponse<{
        id: string
        name: string
        totalItemCount: number
        incompleteItemCount: number
        created: string
      }>(200)
    }
  }
  // ...
})

With --extract-inline-schemas enabled

Notice how this:

  • Creates a lot of duplication, we have to repeat the definition anytime it is used
  • Makes it inconvenient, or impossible to reference the type/schema in our implementation code

With --extract-inline-schemas enabled, the code generator will synthesis a name for each inline schema based on its usage, and emit exported types/schemas, eg:

export type t_getTodoListByIdJson200Response = {
  id: string
  name: string
  totalItemCount: number
  incompleteItemCount: number
  created: string
}
 
export const s_getTodoListByIdJson200Response = z.object({
  id: z.string(),
  name: z.string(),
  totalItemCount: z.coerce.number(),
  incompleteItemCount: z.coerce.number(),
  created: z.string().datetime({ offset: true }),
})

This can be a handy trick to make the code generated from schemas you don’t own/control easier to work with. In general you should prefer to improve the specifications to be more suitable for code generation, which generally also improves the result of documentation tools like Redoc