Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<loremipsum>`, 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({
Expand Down Expand Up @@ -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
Expand All @@ -59,6 +71,10 @@ class Validator {
convertSchemaToAjvFormat (schema) {
if (schema === null) return

if (typeof schema.$ref === 'string') {
schema.$ref = escapeRefForAjv(schema.$ref)
}
Comment on lines +74 to +76

if (schema.type === 'string') {
schema.fjs_type = 'string'
schema.type = ['string', 'object']
Expand Down
42 changes: 42 additions & 0 deletions test/ref.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}')
})
Loading