Skip to content

Always make all references absolute in nested bundled schemas #4683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 1, 2025
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ should change the heading of the (upcoming) version to include a major version b
- Extended `Registry` interface to include optional `experimental_componentUpdateStrategy` property
- Added `shallowEquals()` utility function for shallow equality comparisons

# 6.0.0-beta.13

## rjsf/utils

- Always make all references absolute in nested bundled schemas

# 6.0.0-beta.12

## @rjsf/core
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export const UI_GLOBAL_OPTIONS_KEY = 'ui:globalOptions';

/** The JSON Schema version strings
*/
export const JSON_SCHEMA_DRAFT_2019_09 = 'https://json-schema.org/draft/2019-09/schema';
export const JSON_SCHEMA_DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
9 changes: 8 additions & 1 deletion packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
toIdSchema,
toPathSchema,
} from './schema';
import { makeAllReferencesAbsolute } from './findSchemaDefinition';
import { ID_KEY, JSON_SCHEMA_DRAFT_2020_12, SCHEMA_KEY } from './constants';
import get from 'lodash/get';

/** The `SchemaUtils` class provides a wrapper around the publicly exported APIs in the `utils/schema` directory such
* that one does not have to explicitly pass the `validator`, `rootSchema`, `experimental_defaultFormStateBehavior` or
Expand Down Expand Up @@ -57,7 +60,11 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior,
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>,
) {
this.rootSchema = rootSchema;
if (rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2020_12) {
this.rootSchema = makeAllReferencesAbsolute(rootSchema, get(rootSchema, ID_KEY, '#'));
} else {
this.rootSchema = rootSchema;
}
this.validator = validator;
this.experimental_defaultFormStateBehavior = experimental_defaultFormStateBehavior;
this.experimental_customMergeAllOf = experimental_customMergeAllOf;
Expand Down
54 changes: 51 additions & 3 deletions packages/utils/src/findSchemaDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import jsonpointer from 'jsonpointer';
import omit from 'lodash/omit';

import { ID_KEY, JSON_SCHEMA_DRAFT_2020_12, REF_KEY, SCHEMA_KEY } from './constants';
import {
ALL_OF_KEY,
ID_KEY,
JSON_SCHEMA_DRAFT_2019_09,
JSON_SCHEMA_DRAFT_2020_12,
REF_KEY,
SCHEMA_KEY,
} from './constants';
import { GenericObjectType, RJSFSchema, StrictRJSFSchema } from './types';
import isObject from 'lodash/isObject';
import isEmpty from 'lodash/isEmpty';
Expand All @@ -20,7 +27,16 @@ function findEmbeddedSchemaRecursive<S extends StrictRJSFSchema = RJSFSchema>(sc
return schema;
}
for (const subSchema of Object.values(schema)) {
if (isObject(subSchema)) {
if (Array.isArray(subSchema)) {
for (const item of subSchema) {
if (isObject(item)) {
const result = findEmbeddedSchemaRecursive<S>(item as S, ref);
if (result !== undefined) {
return result as S;
}
}
}
} else if (isObject(subSchema)) {
const result = findEmbeddedSchemaRecursive<S>(subSchema as S, ref);
if (result !== undefined) {
return result as S;
Expand All @@ -30,6 +46,31 @@ function findEmbeddedSchemaRecursive<S extends StrictRJSFSchema = RJSFSchema>(sc
return undefined;
}

/** Parses a JSONSchema and makes all references absolute with respect to
* the `baseURI` argument
* @param schema - The schema to be processed
* @param baseURI - The base URI to be used for resolving relative references
*/
export function makeAllReferencesAbsolute<S extends StrictRJSFSchema = RJSFSchema>(schema: S, baseURI: string): S {
const currentURI = get(schema, ID_KEY, baseURI);
// Make all other references absolute
if (REF_KEY in schema) {
schema = { ...schema, [REF_KEY]: UriResolver.resolve(currentURI, schema[REF_KEY]!) };
}
// Look for references in nested subschemas
for (const [key, subSchema] of Object.entries(schema)) {
if (Array.isArray(subSchema)) {
schema = {
...schema,
[key]: subSchema.map((item) => (isObject(item) ? makeAllReferencesAbsolute(item as S, currentURI) : item)),
};
} else if (isObject(subSchema)) {
schema = { ...schema, [key]: makeAllReferencesAbsolute(subSchema as S, currentURI) };
}
}
return schema;
}

/** Splits out the value at the `key` in `object` from the `object`, returning an array that contains in the first
* location, the `object` minus the `key: value` and in the second location the `value`.
*
Expand Down Expand Up @@ -103,7 +144,14 @@ export function findSchemaDefinitionRecursive<S extends StrictRJSFSchema = RJSFS
const [remaining, theRef] = splitKeyElementFromObject(REF_KEY, current);
const subSchema = findSchemaDefinitionRecursive<S>(theRef, rootSchema, [...recurseList, ref], baseURI);
if (Object.keys(remaining).length > 0) {
return { ...remaining, ...subSchema };
if (
rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2019_09 ||
rootSchema[SCHEMA_KEY] === JSON_SCHEMA_DRAFT_2020_12
) {
return { [ALL_OF_KEY]: [remaining, subSchema] } as S;
} else {
return { ...remaining, ...subSchema };
}
}
return subSchema;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/schema/retrieveSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ export function stubExistingAdditionalProperties<
if (!isEmpty(matchingProperties)) {
schema.properties[key] = retrieveSchema<T, S, F>(
validator,
{ allOf: Object.values(matchingProperties) } as S,
{ [ALL_OF_KEY]: Object.values(matchingProperties) } as S,
rootSchema,
get(formData, [key]) as T,
experimental_customMergeAllOf,
Expand All @@ -445,7 +445,7 @@ export function stubExistingAdditionalProperties<
if (REF_KEY in schema.additionalProperties!) {
additionalProperties = retrieveSchema<T, S, F>(
validator,
{ $ref: get(schema.additionalProperties, [REF_KEY]) } as S,
{ [REF_KEY]: get(schema.additionalProperties, [REF_KEY]) } as S,
rootSchema,
formData as T,
experimental_customMergeAllOf,
Expand Down
35 changes: 35 additions & 0 deletions packages/utils/test/createSchemaUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import {
createSchemaUtils,
Experimental_DefaultFormStateBehavior,
ID_KEY,
JSON_SCHEMA_DRAFT_2020_12,
PROPERTIES_KEY,
REF_KEY,
RJSFSchema,
SCHEMA_KEY,
SchemaUtilsType,
ValidatorType,
} from '../src';
Expand All @@ -23,6 +28,36 @@ describe('createSchemaUtils()', () => {
expect(schemaUtils.getValidator()).toBe(testValidator);
});

describe('2020-12 schema', () => {
const rootSchema2020: RJSFSchema = {
[SCHEMA_KEY]: JSON_SCHEMA_DRAFT_2020_12,
[ID_KEY]: 'https://example.com/2020-12.json',
type: 'object',
$defs: {
example: {
type: 'integer',
},
},
[PROPERTIES_KEY]: {
ref: {
[REF_KEY]: '#/$defs/example',
},
},
};
const schemaUtils2020: SchemaUtilsType = createSchemaUtils(testValidator, rootSchema2020, defaultFormStateBehavior);

it('getRootSchema()', () => {
expect(schemaUtils2020.getRootSchema()).toEqual({
...rootSchema2020,
[PROPERTIES_KEY]: {
ref: {
[REF_KEY]: 'https://example.com/2020-12.json#/$defs/example',
},
},
});
});
});

describe('doesSchemaUtilsDiffer()', () => {
describe('constructed without defaultFormStateBehavior', () => {
const schemaUtils: SchemaUtilsType = createSchemaUtils(testValidator, rootSchema);
Expand Down
Loading