-
Notifications
You must be signed in to change notification settings - Fork 409
core: Remove AJV usage from combinator mappers #2413
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
base: master
Are you sure you want to change the base?
Changes from all commits
138d69d
f83c4a6
659c47e
cf3f154
80405a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -2,6 +2,116 @@ | |||||||
|
||||||||
## Migrating to JSON Forms 3.6 | ||||||||
|
||||||||
### Combinator (anyOf & oneOf) index selection now uses a heuristic instead of AJV | ||||||||
|
||||||||
In this update, we have eliminated the direct usage of AJV to determine the selected subschema for combinator renderers. | ||||||||
To achieve this, the algorithm in `getCombinatorIndexOfFittingSchema` and with this `mapStateToCombinatorRendererProps` was changed. | ||||||||
Thus, custom renderers using either method might have behavior changes. | ||||||||
This rework is part of an ongoing effort to remove mandatory usage of AJV from JSON Forms. | ||||||||
|
||||||||
Before this change, AJV was used to validate the current data against all schemas of the combinator. | ||||||||
This was replaced by a heuristic which tries to match the schema via an identification property against a `const` entry in the schema. | ||||||||
|
||||||||
The identification property is determined as follows in descending order of priority: | ||||||||
|
||||||||
1. The schema contains a new custom property `x-jsf-type-property` next to the combinator to define the identification property. | ||||||||
2. At least one of the combinator schemas has this property with a const declaration: `type`, `kind`. They are considered in the listed order. | ||||||||
|
||||||||
If no combinator schema can be matched, fallback to the first one as before this update. | ||||||||
|
||||||||
Note that this approach can not determine a subschema for non-object subschemas (e.g. ones only defining a primitive property). | ||||||||
Furthermore, subschemas can no longer automatically be selected based on validation results like | ||||||||
produced by different required properties between subschemas. | ||||||||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
#### Example 1: Custom identification property | ||||||||
|
||||||||
Use custom property `x-jsf-type-property` to define which property's content identifies the subschema to select. | ||||||||
In this case, `mytype` is defined as the property to use. The two subschemas in the `anyOf` each define a `const` value for this property. | ||||||||
Meaning a data object with property `mytype: 'user'` results in the second subschema being selected. | ||||||||
The `default` keyword can be used to tell JSON Forms to automatically initialize the property. | ||||||||
|
||||||||
```ts | ||||||||
const schema = { | ||||||||
$schema: 'http://json-schema.org/draft-07/schema#', | ||||||||
type: 'object', | ||||||||
properties: { | ||||||||
addressOrUser: { | ||||||||
'x-jsf-type-property': 'mytype', | ||||||||
anyOf: [ | ||||||||
{ | ||||||||
type: 'object', | ||||||||
properties: { | ||||||||
mytype: { const: 'address', default: 'address' }, | ||||||||
street_address: { type: 'string' }, | ||||||||
city: { type: 'string' }, | ||||||||
state: { type: 'string' }, | ||||||||
}, | ||||||||
}, | ||||||||
{ | ||||||||
type: 'object', | ||||||||
properties: { | ||||||||
mytype: { const: 'user', default: 'user' }, | ||||||||
name: { type: 'string' }, | ||||||||
}, | ||||||||
}, | ||||||||
], | ||||||||
}, | ||||||||
}, | ||||||||
}; | ||||||||
|
||||||||
// Data that results in the second subschema being selected | ||||||||
const dataWithUser = { | ||||||||
addressOrUser: { | ||||||||
mytype: 'user', | ||||||||
name: 'Peter', | ||||||||
}, | ||||||||
}; | ||||||||
``` | ||||||||
|
||||||||
#### Example 2: Use a default identification property | ||||||||
|
||||||||
In this example we use the `kind` property as the identification property. | ||||||||
Like in the custom property case, subschemas are matched via a `const` definition in the identification property's schema. | ||||||||
However, we do not need to explicitly specify `kind` being used. | ||||||||
The `default` keyword can be used to tell JSON Forms to automatically initialize the property. | ||||||||
|
||||||||
```ts | ||||||||
const schema = { | ||||||||
$schema: 'http://json-schema.org/draft-07/schema#', | ||||||||
type: 'object', | ||||||||
properties: { | ||||||||
addressOrUser: { | ||||||||
anyOf: [ | ||||||||
{ | ||||||||
type: 'object', | ||||||||
properties: { | ||||||||
kind: { const: 'address', default: 'address' }, | ||||||||
street_address: { type: 'string' }, | ||||||||
city: { type: 'string' }, | ||||||||
state: { type: 'string' }, | ||||||||
}, | ||||||||
}, | ||||||||
{ | ||||||||
type: 'object', | ||||||||
properties: { | ||||||||
kind: { const: 'user', default: 'user' }, | ||||||||
name: { type: 'string' }, | ||||||||
}, | ||||||||
}, | ||||||||
], | ||||||||
}, | ||||||||
}, | ||||||||
}; | ||||||||
|
||||||||
// Data that results in the second subschema being selected | ||||||||
const dataWithUser = { | ||||||||
addressOrUser: { | ||||||||
kind: 'user', | ||||||||
name: 'Peter', | ||||||||
}, | ||||||||
}; | ||||||||
``` | ||||||||
|
||||||||
### UI schema type changes | ||||||||
|
||||||||
The `UISchemaElement` type was renamed to `BaseUISchemaElement` and a new `UISchemaElement` type was introduced, which is a union of all available UI schema types. | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,12 @@ export interface CombinatorSubSchemaRenderInfo { | |
|
||
export type CombinatorKeyword = 'anyOf' | 'oneOf' | 'allOf'; | ||
|
||
/** Custom schema keyword to define the property identifying different combinator schemas. */ | ||
export const COMBINATOR_TYPE_PROPERTY = 'x-jsf-type-property'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could place these two properties as well as |
||
|
||
/** Default properties that are used to identify combinator schemas. */ | ||
export const COMBINATOR_IDENTIFICATION_PROPERTIES = ['type', 'kind']; | ||
|
||
export const createCombinatorRenderInfos = ( | ||
combinatorSubSchemas: JsonSchema[], | ||
rootSchema: JsonSchema, | ||
|
@@ -67,3 +73,84 @@ export const createCombinatorRenderInfos = ( | |
`${keyword}-${subSchemaIndex}`, | ||
}; | ||
}); | ||
|
||
/** | ||
* Returns the index of the schema in the given combinator keyword that matches the identification property of the given data object. | ||
* The heuristic only works for data objects with a corresponding schema. If the data is a primitive value or an array, the heuristic does not work. | ||
* | ||
* The following heuristics are applied: | ||
* If the schema defines a `x-jsf-type-property`, it is used as the identification property. | ||
* Otherwise, the first of the following properties is used if it exists in at least one combinator schema and has a `const` entry: | ||
* - `type` | ||
* - `kind` | ||
* | ||
* If the index cannot be determined, `-1` is returned. | ||
* | ||
* @returns the index of the fitting schema or `-1` if no fitting schema was found | ||
*/ | ||
export const getCombinatorIndexOfFittingSchema = ( | ||
data: any, | ||
keyword: CombinatorKeyword, | ||
schema: JsonSchema, | ||
rootSchema: JsonSchema | ||
): number => { | ||
if (typeof data !== 'object' || data === null) { | ||
return -1; | ||
} | ||
|
||
// Resolve all schemas in the combinator. | ||
const resolvedCombinatorSchemas = []; | ||
for (let i = 0; i < schema[keyword]?.length; i++) { | ||
let resolvedSchema = schema[keyword][i]; | ||
if (resolvedSchema.$ref) { | ||
resolvedSchema = Resolve.schema( | ||
rootSchema, | ||
resolvedSchema.$ref, | ||
rootSchema | ||
); | ||
} | ||
resolvedCombinatorSchemas.push(resolvedSchema); | ||
} | ||
|
||
// Determine the identification property | ||
let idProperty: string | undefined; | ||
if ( | ||
COMBINATOR_TYPE_PROPERTY in schema && | ||
typeof schema[COMBINATOR_TYPE_PROPERTY] === 'string' | ||
) { | ||
idProperty = schema[COMBINATOR_TYPE_PROPERTY]; | ||
} else { | ||
// Use the first default identification property that has a const entry in at least one of the schemas | ||
for (const potentialIdProp of COMBINATOR_IDENTIFICATION_PROPERTIES) { | ||
for (const resolvedSchema of resolvedCombinatorSchemas) { | ||
if (resolvedSchema.properties?.[potentialIdProp]?.const !== undefined) { | ||
idProperty = potentialIdProp; | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
let indexOfFittingSchema = -1; | ||
if (idProperty === undefined) { | ||
return indexOfFittingSchema; | ||
} | ||
|
||
// Check if the data matches the identification property of one of the resolved schemas | ||
for (let i = 0; i < resolvedCombinatorSchemas.length; i++) { | ||
const resolvedSchema = resolvedCombinatorSchemas[i]; | ||
|
||
// Match the identification property against a constant value in resolvedSchema | ||
const maybeConstIdValue = resolvedSchema.properties?.[idProperty]?.const; | ||
|
||
if ( | ||
maybeConstIdValue !== undefined && | ||
data[idProperty] === maybeConstIdValue | ||
) { | ||
indexOfFittingSchema = i; | ||
break; | ||
} | ||
} | ||
|
||
return indexOfFittingSchema; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should place this for JSON Forms 4.0 as this will break adopters