Skip to content
Draft
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
34 changes: 21 additions & 13 deletions src/prenormalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,7 @@ function resolveRootRef(schema: JSONSchema): void {
}
seenPointers.add(pointer)

const target = pointer
.slice(2)
.split('/')
.reduce<unknown>((node, segment) => {
if (!isPlainObject(node) && !Array.isArray(node)) {
return undefined
}
const key = safeDecodeURIComponent(segment.replace(/~1/g, '/').replace(/~0/g, '~'))
// Only an own property is a real JSON Pointer match -- otherwise a segment
// like `__proto__` would resolve via the prototype chain instead of failing.
return Object.prototype.hasOwnProperty.call(node, key) ? (node as Record<string, unknown>)[key] : undefined
}, documentRoot)
const target = inDocumentPointerValue(documentRoot, pointer)

if (typeof target === 'boolean') {
// A boolean schema, which the root cannot become in place: keep it as the root's one more
Expand All @@ -247,7 +236,7 @@ function resolveRootRef(schema: JSONSchema): void {
break
}
if (!isPlainObject(target)) {
break // not a schema in this document; let $RefParser handle/report it
break // missing, or not a schema: `dereference` reports the latter, $RefParser the former
}

delete schema.$ref
Expand All @@ -262,6 +251,25 @@ function resolveRootRef(schema: JSONSchema): void {
}
}

/**
* The value a `#/...` pointer leads to in `document`: what `resolveRootRef` merges into the root
* when it is a schema, and what `dereference` judges when it is not. Walks plain objects and
* arrays by their own keys only -- otherwise a segment like `__proto__` would resolve via the
* prototype chain instead of failing. Undefined where the pointer leads nowhere.
*/
export function inDocumentPointerValue(document: unknown, pointer: string): unknown {
return pointer
.slice(2)
.split('/')
.reduce<unknown>((node, segment) => {
if (!isPlainObject(node) && !Array.isArray(node)) {
return undefined
}
const key = safeDecodeURIComponent(segment.replace(/~1/g, '/').replace(/~0/g, '~'))
return Object.prototype.hasOwnProperty.call(node, key) ? (node as Record<string, unknown>)[key] : undefined
}, document)
}

// A JSON Pointer segment isn't guaranteed to be a valid percent-encoding (it may
// contain a literal, unescaped `%`), so fall back to the raw segment rather than
// letting decodeURIComponent throw.
Expand Down
12 changes: 11 additions & 1 deletion src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
jsonSchemaParserNormalizeArgs,
} from '@apidevtools/json-schema-ref-parser'
import {isObjectLike, isPlainObject} from 'lodash'
import {prenormalizeDocument} from './prenormalizer'
import {inDocumentPointerValue, prenormalizeDocument} from './prenormalizer'
import {SCHEMA_HOLDING_KEYWORDS} from './keywords'
import {DefinitionKey, JSONSchema, SchemaSource, Source} from './types/JSONSchema'
import {eachSchemaNode, log} from './utils'
Expand Down Expand Up @@ -76,6 +76,16 @@ export async function dereference(
// $RefParser does. A member of a file set goes to $RefParser too: it is registered there under
// its own path, for the other members' `$ref`s to find.
const optionsConcernOtherFiles = Object.keys($refOptions).every(_ => _ === 'resolve' || _ === 'parse')
// The root's own `$ref` into this document was resolved by `prenormalizeDocument` if it led to a
// schema. One that leads to a keyword's value instead (`#/definitions/unit/default`) is judged
// here, before either route: the ref-parser (16 on) follows the root's `$ref` while walking that
// very pointer's tokens, and so reports the pointer as missing when it is not.
if (typeof schema.$ref === 'string' && schema.$ref.startsWith('#/')) {
const target = inDocumentPointerValue(schema, schema.$ref)
if (target !== undefined && !isSchema(target)) {
rejectNonSchemaTargets(schema, target, [], () => ({}))
}
}
const targets = optionsConcernOtherFiles && !set ? inDocumentTargets(schema) : undefined
let dereferencedSchema = schema
if (targets) {
Expand Down
14 changes: 14 additions & 0 deletions test/e2e/refToNonSchemaValue.root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* refToNonSchemaValue.ts, with the `$ref` at the root: the whole schema would be the string
* `"metres"`. (The ref-parser, following the root's own `$ref` while walking the pointer's tokens,
* used to report the pointer as missing -- "Token "definitions" does not exist" -- when it is not.)
*/
export const input = {
$ref: '#/definitions/unit/default',
definitions: {
unit: {type: 'string', default: 'metres'},
},
}

export const error =
/\$ref "#\/definitions\/unit\/default" at the root resolves to the string "metres", which is not a JSON Schema/
Loading