Skip to content

Commit e6cbaf7

Browse files
Added support for bundled JSON Schemas (rjsf-team#4599)
* Added support for bundled JSON Schemas * Fix ajv8 build * Added test for recursive references
1 parent ef80ebb commit e6cbaf7

File tree

10 files changed

+392
-17
lines changed

10 files changed

+392
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ should change the heading of the (upcoming) version to include a major version b
6161
## @rjsf/utils
6262

6363
- Updated the `description` field in field props to be a `string | ReactElement` and added `enableMarkdownInDescription` to the `GlobalUISchemaOptions` interface
64+
- Support for bundled JSON Schemas [#4505](https://github.com/rjsf-team/react-jsonschema-form/issues/4505)
6465

6566
## Dev / docs / playground
6667

6768
- Updated the `snapshot-tests` to disable `getTestId()` for snapshots and updated the `formTests.tsx` to add tests for rich text descriptions for generic fields and the `CheckboxWidget`
6869
- Updated the `uiSchema.md` to document new `enableMarkdownInDescription` prop
6970
- Updated the `playground` to move `daisyui` theme choice after `chakra-ui` and to stop freezing the samples to avoid an `AJV` validation issue
71+
- Added a playground example for bundled JSON Schemas
7072

7173
# 6.0.0-beta.1
7274

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Sample } from './Sample';
2+
3+
const bundledSchema: Sample = {
4+
schema: {
5+
$id: 'https://jsonschema.dev/schemas/examples/non-negative-integer-bundle',
6+
$schema: 'https://json-schema.org/draft/2020-12/schema',
7+
description: 'Must be a non-negative integer',
8+
$defs: {
9+
'https://jsonschema.dev/schemas/mixins/integer': {
10+
$schema: 'https://json-schema.org/draft/2020-12/schema',
11+
$id: 'https://jsonschema.dev/schemas/mixins/integer',
12+
description: 'Must be an integer',
13+
type: 'integer',
14+
},
15+
'https://jsonschema.dev/schemas/mixins/non-negative': {
16+
$schema: 'https://json-schema.org/draft/2020-12/schema',
17+
$id: 'https://jsonschema.dev/schemas/mixins/non-negative',
18+
description: 'Not allowed to be negative',
19+
minimum: 0,
20+
},
21+
nonNegativeInteger: {
22+
allOf: [
23+
{
24+
$ref: '/schemas/mixins/integer',
25+
},
26+
{
27+
$ref: '/schemas/mixins/non-negative',
28+
},
29+
],
30+
},
31+
},
32+
properties: {
33+
num: {
34+
$ref: '#/$defs/nonNegativeInteger',
35+
},
36+
},
37+
},
38+
uiSchema: {},
39+
};
40+
41+
export default bundledSchema;

packages/playground/src/samples/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import customField from './customField';
3434
import layoutGrid from './layoutGrid';
3535
import { Sample } from './Sample';
3636
import patternProperties from './patternProperties';
37+
import bundledSchema from './bundledSchema';
3738

3839
export type { Sample };
3940

@@ -74,6 +75,7 @@ const _samples: Record<string, Sample> = {
7475
Defaults: defaults,
7576
'Custom Field': customField,
7677
'Layout Grid': layoutGrid,
78+
'Bundled Schema': bundledSchema,
7779
};
7880

7981
export const samples = _samples;

packages/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"react": ">=18"
6666
},
6767
"dependencies": {
68+
"fast-uri": "^3.0.6",
6869
"json-schema-merge-allof": "^0.8.1",
6970
"jsonpointer": "^5.0.1",
7071
"lodash": "^4.17.21",

packages/utils/src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const READONLY_KEY = 'readonly';
2525
export const REQUIRED_KEY = 'required';
2626
export const SUBMIT_BTN_OPTIONS_KEY = 'submitButtonOptions';
2727
export const REF_KEY = '$ref';
28+
export const SCHEMA_KEY = '$schema';
2829
/** The path of the discriminator value returned by the schema endpoint.
2930
* The discriminator is the value in a `oneOf` that determines which option is selected.
3031
*/
@@ -42,3 +43,7 @@ export const UI_FIELD_KEY = 'ui:field';
4243
export const UI_WIDGET_KEY = 'ui:widget';
4344
export const UI_OPTIONS_KEY = 'ui:options';
4445
export const UI_GLOBAL_OPTIONS_KEY = 'ui:globalOptions';
46+
47+
/** The JSON Schema version strings
48+
*/
49+
export const JSON_SCHEMA_DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';

packages/utils/src/findSchemaDefinition.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
11
import jsonpointer from 'jsonpointer';
22
import omit from 'lodash/omit';
33

4-
import { REF_KEY } from './constants';
4+
import { ID_KEY, JSON_SCHEMA_DRAFT_2020_12, REF_KEY, SCHEMA_KEY } from './constants';
55
import { GenericObjectType, RJSFSchema, StrictRJSFSchema } from './types';
6+
import isObject from 'lodash/isObject';
7+
import isEmpty from 'lodash/isEmpty';
8+
import UriResolver from 'fast-uri';
9+
10+
/** Looks for the `$id` pointed by `ref` in the schema definitions embedded in
11+
* a JSON Schema bundle
12+
*
13+
* @param schema - The schema wherein `ref` should be searched
14+
* @param ref - The `$id` of the reference to search for
15+
* @returns - The schema matching the reference, or `undefined` if no match is found
16+
*/
17+
function findEmbeddedSchemaRecursive<S extends StrictRJSFSchema = RJSFSchema>(schema: S, ref: string): S | undefined {
18+
if (ID_KEY in schema && UriResolver.equal(schema[ID_KEY] as string, ref)) {
19+
return schema;
20+
}
21+
for (const subSchema of Object.values(schema)) {
22+
if (isObject(subSchema)) {
23+
const result = findEmbeddedSchemaRecursive<S>(subSchema as S, ref);
24+
if (result !== undefined) {
25+
return result as S;
26+
}
27+
}
28+
}
29+
return undefined;
30+
}
631

732
/** Splits out the value at the `key` in `object` from the `object`, returning an array that contains in the first
833
* location, the `object` minus the `key: value` and in the second location the `value`.
@@ -26,23 +51,40 @@ export function splitKeyElementFromObject(key: string, object: GenericObjectType
2651
* @param $ref - The ref string for which the schema definition is desired
2752
* @param [rootSchema={}] - The root schema in which to search for the definition
2853
* @param recurseList - List of $refs already resolved to prevent recursion
54+
* @param baseURI - The base URI to be used for resolving relative references
2955
* @returns - The sub-schema within the `rootSchema` which matches the `$ref` if it exists
3056
* @throws - Error indicating that no schema for that reference could be resolved
3157
*/
3258
export function findSchemaDefinitionRecursive<S extends StrictRJSFSchema = RJSFSchema>(
3359
$ref?: string,
3460
rootSchema: S = {} as S,
3561
recurseList: string[] = [],
62+
baseURI: string | undefined = ID_KEY in rootSchema ? rootSchema[ID_KEY] : undefined,
3663
): S {
3764
const ref = $ref || '';
38-
let decodedRef;
65+
let current: S | undefined = undefined;
3966
if (ref.startsWith('#')) {
4067
// Decode URI fragment representation.
41-
decodedRef = decodeURIComponent(ref.substring(1));
42-
} else {
43-
throw new Error(`Could not find a definition for ${$ref}.`);
68+
const decodedRef = decodeURIComponent(ref.substring(1));
69+
if (baseURI === undefined || (ID_KEY in rootSchema && rootSchema[ID_KEY] === baseURI)) {
70+
current = jsonpointer.get(rootSchema, decodedRef);
71+
} else if (rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2020_12) {
72+
current = findEmbeddedSchemaRecursive<S>(rootSchema, baseURI.replace(/\/$/, ''));
73+
if (current !== undefined) {
74+
current = jsonpointer.get(current, decodedRef);
75+
}
76+
}
77+
} else if (rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2020_12) {
78+
const resolvedRef = baseURI ? UriResolver.resolve(baseURI, ref) : ref;
79+
const [refId, ...refAnchor] = resolvedRef.replace(/#\/?$/, '').split('#');
80+
current = findEmbeddedSchemaRecursive<S>(rootSchema, refId.replace(/\/$/, ''));
81+
if (current !== undefined) {
82+
baseURI = current[ID_KEY];
83+
if (!isEmpty(refAnchor)) {
84+
current = jsonpointer.get(current, decodeURIComponent(refAnchor.join('#')));
85+
}
86+
}
4487
}
45-
const current: S = jsonpointer.get(rootSchema, decodedRef);
4688
if (current === undefined) {
4789
throw new Error(`Could not find a definition for ${$ref}.`);
4890
}
@@ -58,7 +100,7 @@ export function findSchemaDefinitionRecursive<S extends StrictRJSFSchema = RJSFS
58100
throw new Error(`Definition for ${firstRef} contains a circular reference through ${circularPath}`);
59101
}
60102
const [remaining, theRef] = splitKeyElementFromObject(REF_KEY, current);
61-
const subSchema = findSchemaDefinitionRecursive<S>(theRef, rootSchema, [...recurseList, ref]);
103+
const subSchema = findSchemaDefinitionRecursive<S>(theRef, rootSchema, [...recurseList, ref], baseURI);
62104
if (Object.keys(remaining).length > 0) {
63105
return { ...remaining, ...subSchema };
64106
}
@@ -74,13 +116,15 @@ export function findSchemaDefinitionRecursive<S extends StrictRJSFSchema = RJSFS
74116
*
75117
* @param $ref - The ref string for which the schema definition is desired
76118
* @param [rootSchema={}] - The root schema in which to search for the definition
119+
* @param [baseURI=rootSchema['$id']] - The base URI to be used for resolving relative references
77120
* @returns - The sub-schema within the `rootSchema` which matches the `$ref` if it exists
78121
* @throws - Error indicating that no schema for that reference could be resolved
79122
*/
80123
export default function findSchemaDefinition<S extends StrictRJSFSchema = RJSFSchema>(
81124
$ref?: string,
82125
rootSchema: S = {} as S,
126+
baseURI: string | undefined = ID_KEY in rootSchema ? rootSchema[ID_KEY] : undefined,
83127
): S {
84128
const recurseList: string[] = [];
85-
return findSchemaDefinitionRecursive($ref, rootSchema, recurseList);
129+
return findSchemaDefinitionRecursive($ref, rootSchema, recurseList, baseURI);
86130
}

packages/utils/src/schema/retrieveSchema.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ALL_OF_KEY,
1414
ANY_OF_KEY,
1515
DEPENDENCIES_KEY,
16+
ID_KEY,
1617
IF_KEY,
1718
ITEMS_KEY,
1819
ONE_OF_KEY,
@@ -333,12 +334,14 @@ export function resolveReference<T = any, S extends StrictRJSFSchema = RJSFSchem
333334
* @param schema - The schema for which resolving all references is desired
334335
* @param rootSchema - The root schema that will be forwarded to all the APIs
335336
* @param recurseList - List of $refs already resolved to prevent recursion
337+
* @param baseURI - The base URI to be used for resolving relative references
336338
* @returns - given schema will all references resolved or the original schema if no internal `$refs` were resolved
337339
*/
338340
export function resolveAllReferences<S extends StrictRJSFSchema = RJSFSchema>(
339341
schema: S,
340342
rootSchema: S,
341343
recurseList: string[],
344+
baseURI?: string,
342345
): S {
343346
if (!isObject(schema)) {
344347
return schema;
@@ -353,8 +356,11 @@ export function resolveAllReferences<S extends StrictRJSFSchema = RJSFSchema>(
353356
}
354357
recurseList.push($ref!);
355358
// Retrieve the referenced schema definition.
356-
const refSchema = findSchemaDefinition<S>($ref, rootSchema);
359+
const refSchema = findSchemaDefinition<S>($ref, rootSchema, baseURI);
357360
resolvedSchema = { ...refSchema, ...localSchema };
361+
if (ID_KEY in resolvedSchema) {
362+
baseURI = resolvedSchema[ID_KEY];
363+
}
358364
}
359365

360366
if (PROPERTIES_KEY in resolvedSchema) {
@@ -363,7 +369,7 @@ export function resolveAllReferences<S extends StrictRJSFSchema = RJSFSchema>(
363369
resolvedSchema[PROPERTIES_KEY]!,
364370
(result, value, key: string) => {
365371
const childList: string[] = [...recurseList];
366-
result[key] = resolveAllReferences(value as S, rootSchema, childList);
372+
result[key] = resolveAllReferences(value as S, rootSchema, childList, baseURI);
367373
childrenLists.push(childList);
368374
},
369375
{} as RJSFSchema,
@@ -379,7 +385,7 @@ export function resolveAllReferences<S extends StrictRJSFSchema = RJSFSchema>(
379385
) {
380386
resolvedSchema = {
381387
...resolvedSchema,
382-
items: resolveAllReferences(resolvedSchema.items as S, rootSchema, recurseList),
388+
items: resolveAllReferences(resolvedSchema.items as S, rootSchema, recurseList, baseURI),
383389
};
384390
}
385391

@@ -550,6 +556,9 @@ export function retrieveSchemaInternal<
550556
? experimental_customMergeAllOf(resolvedSchema)
551557
: (mergeAllOf(resolvedSchema, {
552558
deep: false,
559+
resolvers: {
560+
$defs: mergeAllOf.options.resolvers.definitions,
561+
},
553562
} as Options) as S);
554563
if (withContainsSchemas.length) {
555564
resolvedSchema.allOf = withContainsSchemas;

0 commit comments

Comments
 (0)