Skip to content

fix: keep external schemas available after compiling a schema with the same $id - #101

Open
DenisCDev wants to merge 1 commit into
fastify:mainfrom
DenisCDev:fix/keep-external-schemas-across-compiles
Open

DenisCDev wants to merge 1 commit into
fastify:mainfrom
DenisCDev:fix/keep-external-schemas-across-compiles

Conversation

@DenisCDev

@DenisCDev DenisCDev commented Oct 28, 2026 •

Copy link
Copy Markdown

responseSchemaCompiler drops the schema being compiled from the external schemas when it has the same $id (#14). It does that by replacing fjsOpts.schema, but fjsOpts is bound once in buildSerializerFactory and shared by every later call. After the first compile the schema is gone for good, and a later { $ref: '<id>#' } fails with Cannot resolve ref.

In Fastify this shows up inside a plugin that has no addSchema of its own, because that context reuses one serializer compiler for all its routes:

// repro.mjs
import Fastify from 'fastify'

const app = Fastify()
app.addSchema({ $id: 'user', type: 'object', properties: { name: { type: 'string' } } })
app.register(async (child) => {
  child.get('/a', { schema: { response: { 200: child.getSchema('user') } } }, async () => ({ name: 'a' }))
  child.get('/b', { schema: { response: { 200: { $ref: 'user#' } } } }, async () => ({ name: 'b' }))
})
await app.ready()
// FST_ERR_SCH_SERIALIZATION_BUILD: Failed building the serialization schema for GET: /b,
// due to error Cannot resolve ref "user#". Schema with id "user" is not found.

It works if the two routes are swapped, or if they are registered on the root instance (the root has its own addSchema, so it builds a new compiler per route). Reproduced on fastify 5.12.7 and on fastify main.

The fix removes the $id from a per-call copy of the options, so the behavior from #14 is kept for each compile and the shared options are left untouched.

Checklist

The new test in test/duplicate-schema.test.js fails on main with Schema with id "schema1" is not found and passes here. npm test (11/11, 100% coverage, tstyche) and npm run lint pass. I also ran fastify's schema and internals tests (test/schema-*.test.js, test/output-validation.test.js and test/internals/*.test.js, 25 files) against fastify main with this compiler in place: 2572 passed, 0 failed, 6 skipped.

…e same $id

When the schema being compiled had an $id that was also among the external
schemas, responseSchemaCompiler removed it by replacing fjsOpts.schema. That
fjsOpts object is bound once per factory call and shared by every later
compilation, so the schema stayed missing afterwards and any later
{ $ref: '<id>#' } failed with "Cannot resolve ref".

Fastify reuses one serializer compiler for all routes of an encapsulated
context that has no addSchema of its own. A route using
fastify.getSchema('user') as a response schema therefore broke every later
route in that plugin that referenced 'user#', depending only on route order.

The duplicate $id is now removed from a per-call copy of the options.

npm test: 11/11 passing, 100% coverage, tstyche 24/24. npm run lint: clean.
The new test fails on main with "Schema with id "schema1" is not found".

Signed-off-by: DenisCDev <coldgreen04@gmail.com>

@SameRainbows SameRainbows left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Validated this locally. Reassigning fjsOpts to a shallow copy with a cloned schema map cleanly prevents the delete mutation from mutating the bound options across subsequent route compilations.

The reproduction scenario with plugin contexts (child.get('/a', ...) followed by child.get('/b', ...) with $ref) is verified, and the test in test/duplicate-schema.test.js exercises the exact sequence without adding overhead.

Ready to merge from my testing.

This branch has not been deployed

No deployments
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.

2 participants