Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a4ae9f5

Browse files
committedJan 31, 2025
core: Remove AJV usage from combinator mappers
- Adapt algorithm to determine the fitting schema index for combinators to no longer use AJV - New heuristic uses identifying properties that should match a const value in the schema - Adapt MaterialOneOfRenderer.test.tsx to fit new heuristic - Describe changes and add examples to migration guide - Adapt some of the anyOf and oneOf examples to custom and new identification properties #2371
1 parent 6af8825 commit a4ae9f5

File tree

9 files changed

+497
-114
lines changed

9 files changed

+497
-114
lines changed
 

‎MIGRATION.md

+112
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,117 @@
11
# Migration guide
22

3+
## Migrating to JSON Forms 3.6
4+
5+
### Combinator (anyOf & oneOf) index selection now uses a heuristic instead of AJV
6+
7+
In this update, we have eliminated the direct usage of AJV to determine the selected subschema for combinator renderers.
8+
To achieve this, the algorithm in `getCombinatorIndexOfFittingSchema` and with this `mapStateToCombinatorRendererProps` was changed.
9+
Thus, custom renderers using either method might have behavior changes.
10+
This rework is part of an ongoing effort to remove mandatory usage of AJV from JSON Forms.
11+
12+
Before this change, AJV was used to validate the current data against all schemas of the combinator.
13+
This was replaced by using a heuristic which tries to match the schema via an identification property
14+
against a `const` entry in the schema.
15+
16+
The identification property is determined as follows in descending order of priority:
17+
18+
1. The schema contains a new custom property `x-jsf-type-property` next to the combinator to define the identification property.
19+
2. The data has any of these properties: `type`, `kind`, `id`. They are considered in the listed order.
20+
3. The data has any string or number property. The first encountered one is used.
21+
22+
If no combinator schema can be matched, fallback to the first one as before this update.
23+
24+
Note that this approach can not determine a subschema for non-object subschemas (e.g. ones only defining a primitive property).
25+
Furthermore, subschemas can no longer automatically be selected based on validation results like
26+
produced by different required properties between subschemas.
27+
28+
#### Example 1: Custom identification property
29+
30+
Use custom property `x-jsf-type-property` to define which property's content identifies the subschema to select.
31+
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.
32+
Meaning a data object with property `mytype: 'user'` results in the second subschema being selected.
33+
The `default` keyword can be used to tell JSON Forms to automatically initialize the property.
34+
35+
```ts
36+
const schema = {
37+
$schema: 'http://json-schema.org/draft-07/schema#',
38+
type: 'object',
39+
properties: {
40+
addressOrUser: {
41+
'x-jsf-type-property': 'mytype',
42+
anyOf: [
43+
{
44+
type: 'object',
45+
properties: {
46+
mytype: { const: 'address', default: 'address' },
47+
street_address: { type: 'string' },
48+
city: { type: 'string' },
49+
state: { type: 'string' },
50+
},
51+
},
52+
{
53+
type: 'object',
54+
properties: {
55+
mytype: { const: 'user', default: 'user' },
56+
name: { type: 'string' },
57+
},
58+
},
59+
],
60+
},
61+
},
62+
};
63+
64+
// Data that results in the second subschema being selected
65+
const dataWithUser = {
66+
addressOrUser: {
67+
mytype: 'user',
68+
name: 'Peter',
69+
},
70+
};
71+
```
72+
73+
#### Example 2: Use a default identification property
74+
75+
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.
76+
The `default` keyword can be used to tell JSON Forms to automatically initialize the property.
77+
78+
```ts
79+
const schema = {
80+
$schema: 'http://json-schema.org/draft-07/schema#',
81+
type: 'object',
82+
properties: {
83+
addressOrUser: {
84+
anyOf: [
85+
{
86+
type: 'object',
87+
properties: {
88+
kind: { const: 'address', default: 'address' },
89+
street_address: { type: 'string' },
90+
city: { type: 'string' },
91+
state: { type: 'string' },
92+
},
93+
},
94+
{
95+
type: 'object',
96+
properties: {
97+
kind: { const: 'user', default: 'user' },
98+
name: { type: 'string' },
99+
},
100+
},
101+
],
102+
},
103+
},
104+
};
105+
106+
// Data that results in the second subschema being selected
107+
const dataWithUser = {
108+
addressOrUser: {
109+
kind: 'user',
110+
name: 'Peter',
111+
},
112+
};
113+
```
114+
3115
## Migrating to JSON Forms 3.5
4116

5117
### Angular support now targets Angular 18 and Angular 19

‎packages/core/src/mappers/combinators.ts

+104
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export interface CombinatorSubSchemaRenderInfo {
3636

3737
export type CombinatorKeyword = 'anyOf' | 'oneOf' | 'allOf';
3838

39+
/** Custom schema keyword to define the property identifying different combinator schemas. */
40+
export const COMBINATOR_TYPE_PROPERTY = 'x-jsf-type-property';
41+
42+
/** Default properties that are used to identify combinator schemas. */
43+
export const COMBINATOR_IDENTIFICATION_PROPERTIES = ['type', 'kind', 'id'];
44+
3945
export const createCombinatorRenderInfos = (
4046
combinatorSubSchemas: JsonSchema[],
4147
rootSchema: JsonSchema,
@@ -67,3 +73,101 @@ export const createCombinatorRenderInfos = (
6773
`${keyword}-${subSchemaIndex}`,
6874
};
6975
});
76+
77+
/**
78+
* Returns the identification property of the given data object.
79+
* The following heuristics are applied:
80+
* If the schema defines a `x-jsf-type-property`, it is used as the identification property.
81+
* Otherwise, the first of the following data properties is used:
82+
* - `type`
83+
* - `kind`
84+
* - `id`
85+
*
86+
* If none of the above properties are present, the first string or number property of the data object is used.
87+
*/
88+
export const getCombinatorIdentificationProp = (
89+
data: any,
90+
schema: JsonSchema
91+
): string | undefined => {
92+
if (typeof data !== 'object' || data === null) {
93+
return undefined;
94+
}
95+
96+
// Determine the identification property
97+
let idProperty: string | undefined;
98+
if (
99+
COMBINATOR_TYPE_PROPERTY in schema &&
100+
typeof schema[COMBINATOR_TYPE_PROPERTY] === 'string'
101+
) {
102+
idProperty = schema[COMBINATOR_TYPE_PROPERTY];
103+
} else {
104+
// Use the first default identification property that is present in the data object
105+
for (const prop of COMBINATOR_IDENTIFICATION_PROPERTIES) {
106+
if (Object.prototype.hasOwnProperty.call(data, prop)) {
107+
idProperty = prop;
108+
break;
109+
}
110+
}
111+
}
112+
113+
// If no identification property was found, use the first string or number property
114+
// of the data object
115+
if (idProperty === undefined) {
116+
for (const key of Object.keys(data)) {
117+
if (typeof data[key] === 'string' || typeof data[key] === 'number') {
118+
idProperty = key;
119+
break;
120+
}
121+
}
122+
}
123+
124+
return idProperty;
125+
};
126+
127+
/**
128+
* Returns the index of the schema in the given combinator keyword that matches the identification property of the given data object.
129+
* 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.
130+
*
131+
* If the index cannot be determined, `-1` is returned.
132+
*
133+
* @returns the index of the fitting schema or `-1` if no fitting schema was found
134+
*/
135+
export const getCombinatorIndexOfFittingSchema = (
136+
data: any,
137+
keyword: CombinatorKeyword,
138+
schema: JsonSchema,
139+
rootSchema: JsonSchema
140+
): number => {
141+
let indexOfFittingSchema = -1;
142+
const idProperty = getCombinatorIdentificationProp(data, schema);
143+
if (idProperty === undefined) {
144+
return indexOfFittingSchema;
145+
}
146+
147+
for (let i = 0; i < schema[keyword]?.length; i++) {
148+
let resolvedSchema = schema[keyword][i];
149+
if (resolvedSchema.$ref) {
150+
resolvedSchema = Resolve.schema(
151+
rootSchema,
152+
resolvedSchema.$ref,
153+
rootSchema
154+
);
155+
}
156+
157+
// Match the identification property against a constant value in resolvedSchema
158+
const maybeConstIdValue = resolvedSchema.properties?.[idProperty]?.const;
159+
160+
if (
161+
maybeConstIdValue !== undefined &&
162+
data[idProperty] === maybeConstIdValue
163+
) {
164+
indexOfFittingSchema = i;
165+
console.debug(
166+
`Data matches the resolved schema for property ${idProperty}`
167+
);
168+
break;
169+
}
170+
}
171+
172+
return indexOfFittingSchema;
173+
};

‎packages/core/src/mappers/renderer.ts

+13-39
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ import {
8484
getUiSchema,
8585
} from '../store';
8686
import { isInherentlyEnabled } from './util';
87-
import { CombinatorKeyword } from './combinators';
87+
import {
88+
CombinatorKeyword,
89+
getCombinatorIndexOfFittingSchema,
90+
} from './combinators';
8891
import isEqual from 'lodash/isEqual';
8992

9093
const move = (array: any[], index: number, delta: number) => {
@@ -1128,43 +1131,12 @@ export const mapStateToCombinatorRendererProps = (
11281131
const { data, schema, rootSchema, i18nKeyPrefix, label, ...props } =
11291132
mapStateToControlProps(state, ownProps);
11301133

1131-
const ajv = state.jsonforms.core.ajv;
1132-
const structuralKeywords = [
1133-
'required',
1134-
'additionalProperties',
1135-
'type',
1136-
'enum',
1137-
'const',
1138-
];
1139-
const dataIsValid = (errors: ErrorObject[]): boolean => {
1140-
return (
1141-
!errors ||
1142-
errors.length === 0 ||
1143-
!errors.find((e) => structuralKeywords.indexOf(e.keyword) !== -1)
1144-
);
1145-
};
1146-
let indexOfFittingSchema: number;
1147-
// TODO instead of compiling the combinator subschemas we can compile the original schema
1148-
// without the combinator alternatives and then revalidate and check the errors for the
1149-
// element
1150-
for (let i = 0; i < schema[keyword]?.length; i++) {
1151-
try {
1152-
let _schema = schema[keyword][i];
1153-
if (_schema.$ref) {
1154-
_schema = Resolve.schema(rootSchema, _schema.$ref, rootSchema);
1155-
}
1156-
const valFn = ajv.compile(_schema);
1157-
valFn(data);
1158-
if (dataIsValid(valFn.errors)) {
1159-
indexOfFittingSchema = i;
1160-
break;
1161-
}
1162-
} catch (error) {
1163-
console.debug(
1164-
"Combinator subschema is not self contained, can't hand it over to AJV"
1165-
);
1166-
}
1167-
}
1134+
const indexOfFittingSchema = getCombinatorIndexOfFittingSchema(
1135+
data,
1136+
keyword,
1137+
schema,
1138+
rootSchema
1139+
);
11681140

11691141
return {
11701142
data,
@@ -1173,7 +1145,9 @@ export const mapStateToCombinatorRendererProps = (
11731145
...props,
11741146
i18nKeyPrefix,
11751147
label,
1176-
indexOfFittingSchema,
1148+
// Fall back to the first schema if none fits
1149+
indexOfFittingSchema:
1150+
indexOfFittingSchema !== -1 ? indexOfFittingSchema : 0,
11771151
uischemas: getUISchemas(state),
11781152
};
11791153
};

‎packages/core/test/mappers/combinators.test.ts

+245-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import test from 'ava';
22
import { ControlElement } from '../../src/models';
3-
import { createCombinatorRenderInfos } from '../../src/mappers';
3+
import {
4+
createCombinatorRenderInfos,
5+
getCombinatorIndexOfFittingSchema,
6+
} from '../../src/mappers';
47

58
const rootSchema = {
69
type: 'object',
@@ -111,3 +114,244 @@ test('createCombinatorRenderInfos - uses keyword + index when no labels provided
111114
t.deepEqual(duaRenderInfo.label, 'anyOf-0');
112115
t.deepEqual(lipaRenderInfo.label, 'anyOf-1');
113116
});
117+
118+
const schemaWithCustomIdProperty = {
119+
properties: {
120+
customId: { const: '123' },
121+
},
122+
};
123+
124+
const schemaWithId = {
125+
properties: {
126+
id: { const: '123' },
127+
},
128+
};
129+
130+
const schemaWithIdWithoutConst = {
131+
properties: {
132+
type: { type: 'string' },
133+
},
134+
};
135+
136+
const schemaWithType = {
137+
properties: {
138+
type: { const: 'typeValue' },
139+
},
140+
};
141+
142+
const schemaWithKind = {
143+
properties: {
144+
kind: { const: 'kindValue' },
145+
},
146+
};
147+
148+
const schemaWithFirstString = {
149+
properties: {
150+
obj: { type: 'object' },
151+
name: { const: 'John' },
152+
},
153+
};
154+
155+
const schemaWithFirstNumber = {
156+
properties: {
157+
obj: { type: 'object' },
158+
identity: { const: 123 },
159+
},
160+
};
161+
162+
const schemaWithFirstNumberWithoutConst = {
163+
properties: {
164+
obj: { type: 'object' },
165+
identity: { type: 'number' },
166+
},
167+
};
168+
169+
const indexRootSchema = {
170+
definitions: {
171+
schemaWithCustomIdProperty,
172+
schemaWithId,
173+
schemaWithIdWithoutConst,
174+
schemaWithType,
175+
schemaWithKind,
176+
schemaWithFirstString,
177+
schemaWithFirstNumber,
178+
schemaWithFirstNumberWithoutConst,
179+
},
180+
};
181+
182+
test('getCombinatorIndexOfFittingSchema - schema with x-jsf-type-property', (t) => {
183+
const data = { customId: '123' };
184+
const keyword = 'anyOf';
185+
const schema = {
186+
anyOf: [schemaWithId, schemaWithCustomIdProperty],
187+
'x-jsf-type-property': 'customId',
188+
};
189+
190+
const result = getCombinatorIndexOfFittingSchema(
191+
data,
192+
keyword,
193+
schema,
194+
indexRootSchema
195+
);
196+
t.is(result, 1);
197+
});
198+
199+
test('getCombinatorIndexOfFittingSchema - data with id property', (t) => {
200+
const data = { id: '123' };
201+
const keyword = 'anyOf';
202+
const schema = { anyOf: [schemaWithId, schemaWithKind] };
203+
204+
const result = getCombinatorIndexOfFittingSchema(
205+
data,
206+
keyword,
207+
schema,
208+
indexRootSchema
209+
);
210+
t.is(result, 0);
211+
});
212+
213+
test('getCombinatorIndexOfFittingSchema - data with id property without const', (t) => {
214+
const data = { id: '123', type: 'typeValue' };
215+
const keyword = 'anyOf';
216+
const schema = { anyOf: [schemaWithIdWithoutConst, schemaWithKind] };
217+
218+
const result = getCombinatorIndexOfFittingSchema(
219+
data,
220+
keyword,
221+
schema,
222+
indexRootSchema
223+
);
224+
// First schema does not have a const and, thus, cannot match
225+
t.is(result, -1);
226+
});
227+
228+
test('getCombinatorIndexOfFittingSchema - data with unfitting id property value', (t) => {
229+
const data = { id: '321' };
230+
const keyword = 'anyOf';
231+
const schema = { anyOf: [schemaWithId, schemaWithKind] };
232+
233+
const result = getCombinatorIndexOfFittingSchema(
234+
data,
235+
keyword,
236+
schema,
237+
indexRootSchema
238+
);
239+
t.is(result, -1);
240+
});
241+
242+
test('getCombinatorIndexOfFittingSchema - data with type property', (t) => {
243+
const data = { type: 'typeValue' };
244+
const keyword = 'anyOf';
245+
const schema = { anyOf: [schemaWithId, schemaWithType] };
246+
247+
const result = getCombinatorIndexOfFittingSchema(
248+
data,
249+
keyword,
250+
schema,
251+
indexRootSchema
252+
);
253+
t.is(result, 1);
254+
});
255+
256+
test('getCombinatorIndexOfFittingSchema - data with unfitting type property value', (t) => {
257+
const data = { type: 'wrongTypeValue' };
258+
const keyword = 'anyOf';
259+
const schema = { anyOf: [schemaWithId, schemaWithType] };
260+
261+
const result = getCombinatorIndexOfFittingSchema(
262+
data,
263+
keyword,
264+
schema,
265+
indexRootSchema
266+
);
267+
t.is(result, -1);
268+
});
269+
270+
test('getCombinatorIndexOfFittingSchema - schema with refs and data with type property', (t) => {
271+
const data = { type: 'typeValue' };
272+
const keyword = 'anyOf';
273+
const schema = {
274+
anyOf: [
275+
{ $ref: '#/definitions/schemaWithId' },
276+
{ $ref: '#/definitions/schemaWithType' },
277+
],
278+
};
279+
280+
const result = getCombinatorIndexOfFittingSchema(
281+
data,
282+
keyword,
283+
schema,
284+
indexRootSchema
285+
);
286+
t.is(result, 1);
287+
});
288+
289+
test('getCombinatorIndexOfFittingSchema - data with kind property', (t) => {
290+
const data = { kind: 'kindValue' };
291+
const keyword = 'anyOf';
292+
const schema = { anyOf: [schemaWithKind] };
293+
294+
const result = getCombinatorIndexOfFittingSchema(
295+
data,
296+
keyword,
297+
schema,
298+
indexRootSchema
299+
);
300+
t.is(result, 0);
301+
});
302+
303+
test('getCombinatorIndexOfFittingSchema - data with unfitting kind property value', (t) => {
304+
const data = { kind: 'wrongKindValue' };
305+
const keyword = 'anyOf';
306+
const schema = { anyOf: [schemaWithKind] };
307+
308+
const result = getCombinatorIndexOfFittingSchema(
309+
data,
310+
keyword,
311+
schema,
312+
indexRootSchema
313+
);
314+
t.is(result, -1);
315+
});
316+
317+
test('getCombinatorIndexOfFittingSchema - data with first string property', (t) => {
318+
const data = { obj: {}, name: 'John' };
319+
const keyword = 'anyOf';
320+
const schema = { anyOf: [{}, schemaWithFirstString] };
321+
322+
const result = getCombinatorIndexOfFittingSchema(
323+
data,
324+
keyword,
325+
schema,
326+
indexRootSchema
327+
);
328+
t.is(result, 1);
329+
});
330+
331+
test('getCombinatorIndexOfFittingSchema - data with first number property', (t) => {
332+
const data = { obj: {}, identity: 123 };
333+
const keyword = 'anyOf';
334+
const schema = { anyOf: [schemaWithFirstNumber] };
335+
336+
const result = getCombinatorIndexOfFittingSchema(
337+
data,
338+
keyword,
339+
schema,
340+
indexRootSchema
341+
);
342+
t.is(result, 0);
343+
});
344+
345+
test('getCombinatorIndexOfFittingSchema - no matching schema', (t) => {
346+
const data = { name: 'Doe' };
347+
const keyword = 'anyOf';
348+
const schema = { anyOf: [schemaWithFirstString] };
349+
350+
const result = getCombinatorIndexOfFittingSchema(
351+
data,
352+
keyword,
353+
schema,
354+
indexRootSchema
355+
);
356+
t.is(result, -1);
357+
});

‎packages/core/test/mappers/renderer.test.ts

-64
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ import {
6363
mapStateToLayoutProps,
6464
mapStateToMultiEnumControlProps,
6565
mapStateToOneOfEnumControlProps,
66-
mapStateToOneOfProps,
6766
} from '../../src/mappers';
6867
import { clearAllIds, convertDateToString, createAjv } from '../../src/util';
6968
import { rankWith } from '../../src';
@@ -1210,69 +1209,6 @@ test('mapStateToLayoutProps - hidden via state with path from ownProps ', (t) =>
12101209
t.false(props.visible);
12111210
});
12121211

1213-
test("mapStateToOneOfProps - indexOfFittingSchema should not select schema if enum doesn't match", (t) => {
1214-
const uischema: ControlElement = {
1215-
type: 'Control',
1216-
scope: '#/properties/method',
1217-
};
1218-
1219-
const ownProps = {
1220-
uischema,
1221-
};
1222-
1223-
const state = {
1224-
jsonforms: {
1225-
core: {
1226-
ajv: createAjv(),
1227-
schema: {
1228-
type: 'object',
1229-
properties: {
1230-
method: {
1231-
oneOf: [
1232-
{
1233-
title: 'Injection',
1234-
type: 'object',
1235-
properties: {
1236-
method: {
1237-
title: 'Method',
1238-
type: 'string',
1239-
enum: ['Injection'],
1240-
default: 'Injection',
1241-
},
1242-
},
1243-
required: ['method'],
1244-
},
1245-
{
1246-
title: 'Infusion',
1247-
type: 'object',
1248-
properties: {
1249-
method: {
1250-
title: 'Method',
1251-
type: 'string',
1252-
enum: ['Infusion'],
1253-
default: 'Infusion',
1254-
},
1255-
},
1256-
required: ['method'],
1257-
},
1258-
],
1259-
},
1260-
},
1261-
},
1262-
data: {
1263-
method: {
1264-
method: 'Infusion',
1265-
},
1266-
},
1267-
uischema,
1268-
},
1269-
},
1270-
};
1271-
1272-
const oneOfProps = mapStateToOneOfProps(state, ownProps);
1273-
t.is(oneOfProps.indexOfFittingSchema, 1);
1274-
});
1275-
12761212
test('mapStateToMultiEnumControlProps - oneOf items', (t) => {
12771213
const uischema: ControlElement = {
12781214
type: 'Control',

‎packages/examples/src/examples/anyOf.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const schema = {
3131
address: {
3232
type: 'object',
3333
properties: {
34+
mytype: { const: 'address', default: 'address' },
3435
street_address: { type: 'string' },
3536
city: { type: 'string' },
3637
state: { type: 'string' },
@@ -40,6 +41,7 @@ export const schema = {
4041
user: {
4142
type: 'object',
4243
properties: {
44+
mytype: { const: 'user', default: 'user' },
4345
name: { type: 'string' },
4446
mail: { type: 'string' },
4547
},
@@ -63,6 +65,7 @@ export const schema = {
6365
{ $ref: '#/definitions/address' },
6466
{ $ref: '#/definitions/user' },
6567
],
68+
'x-jsf-type-property': 'mytype',
6669
},
6770
addressesOrUsers: {
6871
anyOf: [
@@ -104,9 +107,9 @@ export const uischema = {
104107

105108
const data = {
106109
addressOrUser: {
107-
street_address: '1600 Pennsylvania Avenue NW',
108-
city: 'Washington',
109-
state: 'DC',
110+
mytype: 'user',
111+
name: 'Peter',
112+
email: 'peter@example.org',
110113
},
111114
};
112115

‎packages/examples/src/examples/oneOf.ts

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const schema = {
3131
address: {
3232
type: 'object',
3333
properties: {
34+
type: { const: 'address', default: 'address' },
3435
street_address: { type: 'string' },
3536
city: { type: 'string' },
3637
state: { type: 'string' },
@@ -41,6 +42,7 @@ export const schema = {
4142
user: {
4243
type: 'object',
4344
properties: {
45+
type: { const: 'user', default: 'user' },
4446
name: { type: 'string' },
4547
mail: { type: 'string' },
4648
},
@@ -80,6 +82,7 @@ export const uischema = {
8082
const data = {
8183
name: 'test',
8284
addressOrUser: {
85+
type: 'user',
8386
name: 'User',
8487
mail: 'mail@example.com',
8588
},

‎packages/examples/src/examples/oneOfArray.ts

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const schema = {
3131
address: {
3232
type: 'object',
3333
properties: {
34+
kind: { const: 'address', default: 'address' },
3435
street_address: { type: 'string' },
3536
city: { type: 'string' },
3637
state: { type: 'string' },
@@ -40,6 +41,7 @@ export const schema = {
4041
user: {
4142
type: 'object',
4243
properties: {
44+
kind: { const: 'user', default: 'user' },
4345
name: { type: 'string' },
4446
mail: { type: 'string' },
4547
},
@@ -77,11 +79,13 @@ const data = {
7779
name: 'test',
7880
addressOrUsers: [
7981
{
82+
kind: 'address',
8083
street_address: '1600 Pennsylvania Avenue NW',
8184
city: 'Washington',
8285
state: 'DC',
8386
},
8487
{
88+
kind: 'user',
8589
name: 'User',
8690
mail: 'user@user.user',
8791
},

‎packages/material-renderers/test/renderers/MaterialOneOfRenderer.test.tsx

+10-7
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('Material oneOf renderer', () => {
114114
expect(firstTab.html()).toContain('Mui-selected');
115115
});
116116

117-
it('should render and select second tab due to datatype', () => {
117+
it('should render and select first tab for primitive combinator data', () => {
118118
const schema = {
119119
type: 'object',
120120
properties: {
@@ -150,11 +150,11 @@ describe('Material oneOf renderer', () => {
150150
);
151151
expect(wrapper.find(MaterialOneOfRenderer).length).toBeTruthy();
152152

153-
const secondTab = wrapper.find(Tab).at(1);
153+
const secondTab = wrapper.find(Tab).at(0);
154154
expect(secondTab.html()).toContain('Mui-selected');
155155
});
156156

157-
it('should render and select second tab due to schema with additionalProperties', () => {
157+
it('should render and select second tab due to string prop with matching const value', () => {
158158
const schema = {
159159
type: 'object',
160160
properties: {
@@ -164,15 +164,15 @@ describe('Material oneOf renderer', () => {
164164
title: 'String',
165165
type: 'object',
166166
properties: {
167-
foo: { type: 'string' },
167+
foo: { type: 'string', const: 'foo' },
168168
},
169169
additionalProperties: false,
170170
},
171171
{
172172
title: 'Number',
173173
type: 'object',
174174
properties: {
175-
bar: { type: 'string' },
175+
bar: { type: 'string', const: 'bar' },
176176
},
177177
additionalProperties: false,
178178
},
@@ -202,7 +202,7 @@ describe('Material oneOf renderer', () => {
202202
expect(secondTab.html()).toContain('Mui-selected');
203203
});
204204

205-
it('should render and select second tab due to schema with required', () => {
205+
it('should render and select second tab due const custom property', () => {
206206
const schema = {
207207
type: 'object',
208208
properties: {
@@ -213,6 +213,7 @@ describe('Material oneOf renderer', () => {
213213
type: 'object',
214214
properties: {
215215
foo: { type: 'string' },
216+
myprop: { const: 'foo' },
216217
},
217218
required: ['foo'],
218219
},
@@ -221,10 +222,12 @@ describe('Material oneOf renderer', () => {
221222
type: 'object',
222223
properties: {
223224
bar: { type: 'string' },
225+
myprop: { const: 'bar' },
224226
},
225227
required: ['bar'],
226228
},
227229
],
230+
'x-jsf-type-property': 'myprop',
228231
},
229232
},
230233
};
@@ -234,7 +237,7 @@ describe('Material oneOf renderer', () => {
234237
scope: '#/properties/value',
235238
};
236239
const data = {
237-
value: { bar: 'bar' },
240+
value: { bar: 'bar', myprop: 'bar' },
238241
};
239242
wrapper = mount(
240243
<JsonForms

0 commit comments

Comments
 (0)
Please sign in to comment.