Skip to content

Commit f85fe94

Browse files
epicfaaceedi9999
authored andcommitted
Rewrite mergeSchemas to fix schema dependencies merging (rjsf-team#1476)
* rewrite mergeSchemas to fix duplicate error * fix: don't include duplicate values when merging "required" fields * fix: merge required attribute only for object types * fix logic for calculating required keyword
1 parent e130b85 commit f85fe94

File tree

3 files changed

+215
-3
lines changed

3 files changed

+215
-3
lines changed

src/utils.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import * as ReactIs from "react-is";
33
import fill from "core-js/library/fn/array/fill";
44
import validateFormData, { isValid } from "./validate";
5+
import { union } from "lodash";
56

67
export const ADDITIONAL_PROPERTY_FLAG = "__additional_property";
78

@@ -66,6 +67,7 @@ export function getDefaultRegistry() {
6667
};
6768
}
6869

70+
/* Gets the type of a given schema. */
6971
export function getSchemaType(schema) {
7072
let { type } = schema;
7173

@@ -760,8 +762,34 @@ function withExactlyOneSubschema(
760762
);
761763
}
762764

763-
function mergeSchemas(schema1, schema2) {
764-
return mergeObjects(schema1, schema2, true);
765+
// Recursively merge deeply nested schemas.
766+
// The difference between mergeSchemas and mergeObjects
767+
// is that mergeSchemas only concats arrays for
768+
// values under the "required" keyword, and when it does,
769+
// it doesn't include duplicate values.
770+
export function mergeSchemas(obj1, obj2) {
771+
var acc = Object.assign({}, obj1); // Prevent mutation of source object.
772+
return Object.keys(obj2).reduce((acc, key) => {
773+
const left = obj1 ? obj1[key] : {},
774+
right = obj2[key];
775+
if (obj1 && obj1.hasOwnProperty(key) && isObject(right)) {
776+
acc[key] = mergeSchemas(left, right);
777+
} else if (
778+
obj1 &&
779+
obj2 &&
780+
(getSchemaType(obj1) === "object" || getSchemaType(obj2) === "object") &&
781+
key === "required" &&
782+
Array.isArray(left) &&
783+
Array.isArray(right)
784+
) {
785+
// Don't include duplicate values when merging
786+
// "required" fields.
787+
acc[key] = union(left, right);
788+
} else {
789+
acc[key] = right;
790+
}
791+
return acc;
792+
}, acc);
765793
}
766794

767795
function isArguments(object) {

test/Form_test.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2325,7 +2325,44 @@ describeRepeated("Form common", createFormComponent => {
23252325
});
23262326
});
23272327

2328-
describe("Dependency defaults", () => {
2328+
describe("Dependencies", () => {
2329+
it("should not give a validation error by duplicating enum values in dependencies", () => {
2330+
const schema = {
2331+
title: "A registration form",
2332+
description: "A simple form example.",
2333+
type: "object",
2334+
properties: {
2335+
type1: {
2336+
type: "string",
2337+
title: "Type 1",
2338+
enum: ["FOO", "BAR", "BAZ"],
2339+
},
2340+
type2: {
2341+
type: "string",
2342+
title: "Type 2",
2343+
enum: ["GREEN", "BLUE", "RED"],
2344+
},
2345+
},
2346+
dependencies: {
2347+
type1: {
2348+
properties: {
2349+
type1: {
2350+
enum: ["FOO"],
2351+
},
2352+
type2: {
2353+
enum: ["GREEN"],
2354+
},
2355+
},
2356+
},
2357+
},
2358+
};
2359+
const formData = {
2360+
type1: "FOO",
2361+
};
2362+
const { node, comp } = createFormComponent({ schema, formData });
2363+
Simulate.submit(node);
2364+
expect(comp.state.errors).to.have.length.of(0);
2365+
});
23292366
it("should show dependency defaults for uncontrolled components", () => {
23302367
const schema = {
23312368
type: "object",

test/utils_test.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
toIdSchema,
2424
toPathSchema,
2525
guessType,
26+
mergeSchemas,
2627
} from "../src/utils";
2728

2829
describe("utils", () => {
@@ -1058,6 +1059,123 @@ describe("utils", () => {
10581059
});
10591060
});
10601061

1062+
describe("mergeSchemas()", () => {
1063+
it("shouldn't mutate the provided objects", () => {
1064+
const obj1 = { a: 1 };
1065+
mergeSchemas(obj1, { b: 2 });
1066+
expect(obj1).eql({ a: 1 });
1067+
});
1068+
1069+
it("should merge two one-level deep objects", () => {
1070+
expect(mergeSchemas({ a: 1 }, { b: 2 })).eql({ a: 1, b: 2 });
1071+
});
1072+
1073+
it("should override the first object with the values from the second", () => {
1074+
expect(mergeSchemas({ a: 1 }, { a: 2 })).eql({ a: 2 });
1075+
});
1076+
1077+
it("should override non-existing values of the first object with the values from the second", () => {
1078+
expect(mergeSchemas({ a: { b: undefined } }, { a: { b: { c: 1 } } })).eql(
1079+
{ a: { b: { c: 1 } } }
1080+
);
1081+
});
1082+
1083+
it("should recursively merge deeply nested objects", () => {
1084+
const obj1 = {
1085+
a: 1,
1086+
b: {
1087+
c: 3,
1088+
d: [1, 2, 3],
1089+
e: { f: { g: 1 } },
1090+
},
1091+
c: 2,
1092+
};
1093+
const obj2 = {
1094+
a: 1,
1095+
b: {
1096+
d: [3, 2, 1],
1097+
e: { f: { h: 2 } },
1098+
g: 1,
1099+
},
1100+
c: 3,
1101+
};
1102+
const expected = {
1103+
a: 1,
1104+
b: {
1105+
c: 3,
1106+
d: [3, 2, 1],
1107+
e: { f: { g: 1, h: 2 } },
1108+
g: 1,
1109+
},
1110+
c: 3,
1111+
};
1112+
expect(mergeSchemas(obj1, obj2)).eql(expected);
1113+
});
1114+
1115+
it("should recursively merge File objects", () => {
1116+
const file = new File(["test"], "test.txt");
1117+
const obj1 = {
1118+
a: {},
1119+
};
1120+
const obj2 = {
1121+
a: file,
1122+
};
1123+
expect(mergeSchemas(obj1, obj2).a).instanceOf(File);
1124+
});
1125+
1126+
describe("arrays", () => {
1127+
it("should not concat arrays", () => {
1128+
const obj1 = { a: [1] };
1129+
const obj2 = { a: [2] };
1130+
1131+
expect(mergeSchemas(obj1, obj2)).eql({ a: [2] });
1132+
});
1133+
1134+
it("should concat arrays under 'required' keyword", () => {
1135+
const obj1 = { type: "object", required: [1] };
1136+
const obj2 = { type: "object", required: [2] };
1137+
1138+
expect(mergeSchemas(obj1, obj2)).eql({
1139+
type: "object",
1140+
required: [1, 2],
1141+
});
1142+
});
1143+
1144+
it("should concat arrays under 'required' keyword when one of the schemas is an object type", () => {
1145+
const obj1 = { type: "object", required: [1] };
1146+
const obj2 = { required: [2] };
1147+
1148+
expect(mergeSchemas(obj1, obj2)).eql({
1149+
type: "object",
1150+
required: [1, 2],
1151+
});
1152+
});
1153+
1154+
it("should concat nested arrays under 'required' keyword", () => {
1155+
const obj1 = { a: { type: "object", required: [1] } };
1156+
const obj2 = { a: { type: "object", required: [2] } };
1157+
1158+
expect(mergeSchemas(obj1, obj2)).eql({
1159+
a: { type: "object", required: [1, 2] },
1160+
});
1161+
});
1162+
1163+
it("should not include duplicate values when concatting arrays under 'required' keyword", () => {
1164+
const obj1 = { type: "object", required: [1] };
1165+
const obj2 = { type: "object", required: [1] };
1166+
1167+
expect(mergeSchemas(obj1, obj2)).eql({ type: "object", required: [1] });
1168+
});
1169+
1170+
it("should not concat arrays under 'required' keyword that are not under an object type", () => {
1171+
const obj1 = { required: [1] };
1172+
const obj2 = { required: [2] };
1173+
1174+
expect(mergeSchemas(obj1, obj2)).eql({ required: [2] });
1175+
});
1176+
});
1177+
});
1178+
10611179
describe("retrieveSchema()", () => {
10621180
it("should 'resolve' a schema which contains definitions", () => {
10631181
const schema = { $ref: "#/definitions/address" };
@@ -1320,6 +1438,35 @@ describe("utils", () => {
13201438
required: ["a", "b"],
13211439
});
13221440
});
1441+
it("should not concat enum properties, but should concat 'required' properties", () => {
1442+
const schema = {
1443+
type: "object",
1444+
properties: {
1445+
a: { type: "string", enum: ["FOO", "BAR", "BAZ"] },
1446+
b: { type: "string", enum: ["GREEN", "BLUE", "RED"] },
1447+
},
1448+
required: ["a"],
1449+
dependencies: {
1450+
a: {
1451+
properties: {
1452+
a: { enum: ["FOO"] },
1453+
b: { enum: ["BLUE"] },
1454+
},
1455+
required: ["a", "b"],
1456+
},
1457+
},
1458+
};
1459+
const definitions = {};
1460+
const formData = { a: "FOO" };
1461+
expect(retrieveSchema(schema, definitions, formData)).eql({
1462+
type: "object",
1463+
properties: {
1464+
a: { type: "string", enum: ["FOO"] },
1465+
b: { type: "string", enum: ["BLUE"] },
1466+
},
1467+
required: ["a", "b"],
1468+
});
1469+
});
13231470
});
13241471

13251472
describe("with $ref in dependency", () => {

0 commit comments

Comments
 (0)