Skip to content

Conversation

cyky
Copy link
Contributor

@cyky cyky commented Jul 10, 2025

When using TypeBox Modules to define Fastify schemas the current implementation duplicates definitions even when using fastify.addSchema(schema). This causes the openapi schema file to be larger and contain duplicate definitions.

If I am correct the OpenApi specification defines that if $ref key is defined for object there shouldn't exist other keys on the object (meaning $defs shouldn't be there). That is why I've added extra check to convertJsonSchemaToOpenapi3to remove $defs-key if $refs-key exits.

I'm open to suggestions if there's a more appropriate way to resolve this.

Usage example with TypeBox modules:

schema.ts

export const HumanModule = Type.Module({
  AddressSchema: Type.Object({
    street: Type.String(),
    streetNumber: Type.Number(),
  }),
  PersonSchema: Type.Object({
    name: Type.String(),
    homeAddress: Type.Ref('AddressSchema'),
    workAddress: Type.Ref('AddressSchema'),
  }),
  PostRequestSchema: Type.Object({
    person: Type.Ref('PersonSchema'),
  }),
});

export const CommonSchema = {
  $id: 'common',
  humanModule: HumanModule,
};

export const PersonSchema = {
  get: {
    response: {
      200: HumanModule.Import('PersonSchema'),
    },
  },
};

index.ts

const fastify = Fastify();

...

app.addSchema(CommonSchema);
app.get(
  '/person',
  {
    schema: {
      response: {
        200: HumanModule.Import('PersonSchema'),
      },
    },
  },
  async (_request, reply) => {
    return reply.status(200);
  }
);

...

await app.ready();
app.swagger();

After the change and using the above style TypeBox module schema definition results in following openapi json:

{
....
  "components": {
    "schemas": {
      "def-0": {
        "humanModule": {
          "$defs": {
            "AddressSchema": {
              "type": "object",
              "properties": {
                "street": { "type": "string" },
                "streetNumber": { "type": "number" }
              },
              "required": ["street", "streetNumber"],
              "title": "AddressSchema"
            },
            "PersonSchema": {
              "type": "object",
              "properties": {
                "name": { "type": "string" },
                "homeAddress": { "$ref": "#/components/schemas/def-1" },
                "workAddress": { "$ref": "#/components/schemas/def-1" }
              },
              "required": ["name", "homeAddress", "workAddress"],
              "title": "PersonSchema"
            },
            "PostRequestSchema": {
              "type": "object",
              "properties": {
                "person": { "$ref": "#/components/schemas/def-2" }
              },
              "required": ["person"],
              "title": "PostRequestSchema"
            }
          }
        },
        "title": "common"
      },
      "def-1": {
        "type": "object",
        "properties": {
          "street": { "type": "string" },
          "streetNumber": { "type": "number" }
        },
        "required": ["street", "streetNumber"],
        "title": "AddressSchema"
      },
      "def-2": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "homeAddress": { "$ref": "#/components/schemas/def-1" },
          "workAddress": { "$ref": "#/components/schemas/def-1" }
        },
        "required": ["name", "homeAddress", "workAddress"],
        "title": "PersonSchema"
      },
      "def-3": {
        "type": "object",
        "properties": { "person": { "$ref": "#/components/schemas/def-2" } },
        "required": ["person"],
        "title": "PostRequestSchema"
      }
    }
  },
  "paths": {
    "/person": {
      "get": {
        "responses": {
          "200": {
            "description": "Default Response",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/def-2" }
              }
            }
          }
        }
      }
    }
  }
  ....
}

This PR should fix issue related to #854

Related issue on TypeBox repository: sinclairzx81/typebox#1283

Checklist

- Remove $defs from schema keys if $ref key already exists when
  converting json schema into openapi 3. This fixes issue in openapi
  generation when using TypeBox modules to define schema.
- Add test case for refs.
@Fdawgs Fdawgs linked an issue Jul 10, 2025 that may be closed by this pull request
2 tasks
@Fdawgs Fdawgs changed the title Fix #854: Remove defs when ref already defined in schema fix: remove defs when ref already defined in schema Jul 10, 2025
Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks correct

@mcollina mcollina requested a review from Copilot July 11, 2025 05:54
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR removes redundant $defs when an object schema already contains a $ref, preventing duplicate definitions in the generated OpenAPI output. It adds a guard in the convertJsonSchemaToOpenapi3 utility to drop $defs when $ref is present and includes a new test to verify this behavior.

  • Strip $defs when $ref is detected in schema conversion
  • Add a test case to confirm only the $ref remains and definitions are not duplicated

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
lib/spec/openapi/utils.js Added a check to delete $defs if $ref exists in the schema
test/spec/openapi/refs.test.js New test to validate that only $ref is returned when both keys are present
Comments suppressed due to low confidence (1)

test/spec/openapi/refs.test.js:561

  • Consider adding an assertion that the final component schemas do not contain any $defs keys to ensure the removal logic is fully covered.
  t.assert.deepStrictEqual(openapiObject.paths['/person'].get.responses[200].content, expectedPathContent)

Copy link
Member

@ivan-tymoshenko ivan-tymoshenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Hi, I don't agree with a statement if $ref key is defined for object there shouldn't exist other keys on the object. I would say the correct one is Any sibling elements of a $ref are ignored., so this is still a valid schema.

  2. Why do you remove only $defs then and not all other properties except $ref?

  3. My problem with this change is that it might break some references. For example you can define a schema in the $defs that would have a global id $id: Book. If someone has a ref to this schema $ref: Book it will be broken after this change.

Although the change seems logical, I fear it could break the code in cases where people (including us) don't follow the spec exactly.

@cyky
Copy link
Contributor Author

cyky commented Jul 11, 2025

Seems logical that there should be no siblings for $ref. I could change this to remove all other keys if that follows the spec more closely?

I've created this change because we are using TypeBox to create schemas for our routes and without the change the openapi.json size grows quite large as we have large number of shared types between our routes.

I don't quite understand your how you are using this in a way that doesn't follow OpenAPI specification?

@ivan-tymoshenko
Copy link
Member

Sorry, I misunderstood the place where the change is done. For the openapi v3 schemas only it should be fine. Please remove all properties to be consistent (not only $defs).

- Remove sibling keys if $refs exits on openapi 3 json schema.
@cyky
Copy link
Contributor Author

cyky commented Jul 15, 2025

I updated the code to remove siblings if $refs is defined.

@cyky
Copy link
Contributor Author

cyky commented Jul 22, 2025

Is it possible to get this merged or are there some steps that need to be done before that?

@cyky
Copy link
Contributor Author

cyky commented Sep 1, 2025

Ping. Would It be possible to get this merged and released?

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@gurgunday gurgunday merged commit 56dbc02 into fastify:main Sep 25, 2025
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Invalid references in schema after using TypeBox Modules
4 participants