From 46acefded80bafb55f925bfbde4496e36031638f Mon Sep 17 00:00:00 2001 From: DucMinhNe <115204145+DucMinhNe@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:12:41 +0700 Subject: [PATCH] fix: resolve percent-encoded $ref definition keys Ajv resolves a $ref's JSON pointer by url-decoding each segment, so a definition whose key is itself percent-encoded (e.g. Some%3Cloremipsum%3E) could no longer be found once the referenced schema contained oneOf/anyOf/allOf and Ajv compiled a sub-validator for it: the pointer #/definitions/Some%3Cloremipsum%3E decodes to Some, which does not match the literal key, throwing MissingRefError. Re-encode the percent signs in the ref fragment before handing it to Ajv so its single decode round-trips back to the original key. The internal json-schema-ref-resolver (which performs a literal lookup) is untouched. Fixes #740 --- lib/validator.js | 18 +++++++++++++++++- test/ref.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/validator.js b/lib/validator.js index 77bbd101..d29b2292 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -5,6 +5,18 @@ const fastUri = require('fast-uri') const ajvFormats = require('ajv-formats') const clone = require('rfdc')({ proto: true }) +// Ajv resolves a $ref's JSON pointer by url-decoding each segment, so a +// definition whose key is itself percent-encoded (e.g. `Some%3Cloremipsum%3E`) +// can no longer be found: the pointer `#/definitions/Some%3Cloremipsum%3E` +// decodes to `Some`, which does not match the literal key. +// Re-encoding the `%` in the fragment makes Ajv's single decode round-trip +// back to the original key. See https://github.com/fastify/fast-json-stringify/issues/740 +function escapeRefForAjv (ref) { + const hashIndex = ref.indexOf('#') + if (hashIndex === -1) return ref + return ref.slice(0, hashIndex) + ref.slice(hashIndex).replace(/%/g, '%25') +} + class Validator { constructor (ajvOptions) { this.ajv = new Ajv({ @@ -48,7 +60,7 @@ class Validator { } validate (schemaRef, data) { - return this.ajv.validate(schemaRef, data) + return this.ajv.validate(escapeRefForAjv(schemaRef), data) } // Ajv does not natively support JavaScript objects like Date or other types @@ -59,6 +71,10 @@ class Validator { convertSchemaToAjvFormat (schema) { if (schema === null) return + if (typeof schema.$ref === 'string') { + schema.$ref = escapeRefForAjv(schema.$ref) + } + if (schema.type === 'string') { schema.fjs_type = 'string' schema.type = ['string', 'object'] diff --git a/test/ref.test.js b/test/ref.test.js index 2f736445..84937210 100644 --- a/test/ref.test.js +++ b/test/ref.test.js @@ -2075,3 +2075,45 @@ test('ref nested', (t) => { t.assert.doesNotThrow(() => JSON.parse(output)) t.assert.equal(output, '{"str":"test"}') }) + +test('ref internal - percent-encoded definition key with oneOf', (t) => { + t.plan(2) + + // See https://github.com/fastify/fast-json-stringify/issues/740 + // A definition key that is itself percent-encoded (e.g. `Some%3Cloremipsum%3E`) + // must still resolve when the referenced schema contains oneOf/anyOf/allOf. + const schema = { + title: 'object with $ref', + definitions: { + 'Some%3Cloremipsum%3E': { + type: 'object', + additionalProperties: { + oneOf: [ + { type: 'string' }, + { type: 'number' }, + { type: 'object' }, + { type: 'null' } + ] + } + } + }, + type: 'object', + properties: { + obj: { + $ref: '#/definitions/Some%3Cloremipsum%3E' + } + } + } + + const object = { + obj: { + str: 'test' + } + } + + const stringify = build(schema) + const output = stringify(object) + + t.assert.doesNotThrow(() => JSON.parse(output)) + t.assert.equal(output, '{"obj":{"str":"test"}}') +})