History

Bump tool source cache version to v24 and enhance JSON Schema type hinting for unions and object variants

Rhys Sullivan committed 8 months ago

Commit ae17ce31 parent 34a8eeb3 files · +559 −11Browse files

Unified · Split

3 files
  1. executor/packages/convex/runtime/tool_source_loading.ts +1 −1
  2. executor/packages/core/src/openapi/schema-hints.test.ts +206 −3
  3. executor/packages/core/src/openapi/schema-hints.ts +352 −7
executor/packages/convex/runtime/tool_source_loading.ts+1−1
@@ -25,7 +25,7 @@ import { asPayload } from "../lib/object"; · 24 unmodified lines
2525 const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000;
2626
2727 /** Cache version - bump when tool snapshot/registry/type-hint semantics change. */
28−const TOOL_SOURCE_CACHE_VERSION = "v20";
28+const TOOL_SOURCE_CACHE_VERSION = "v24";
2929
3030 export function sourceSignature(workspaceId: string, sources: Array<{ id: string; updatedAt: number; enabled: boolean }>): string {
3131 const parts = sources
executor/packages/core/src/openapi/schema-hints.test.ts+206−3
@@ -52,9 +52,8 @@ test("jsonSchemaTypeHintFallback factors common object fields in oneOf", () => { · 51 unmodified lines
5252 const hint = jsonSchemaTypeHintFallback(schema);
5353 expect(hint).toContain("domain: string");
5454 expect(hint).toContain("value: string");
55− expect(jsonSchemaTypeHintFallback(schema)).toContain("& (");
56− expect(jsonSchemaTypeHintFallback(schema)).toContain("type: \"A\"");
57− expect(jsonSchemaTypeHintFallback(schema)).toContain("type: \"AAAA\"");
55+ // Variants only differ by the discriminant; they should be merged.
56+ expect(hint).toContain("type: \"A\" | \"AAAA\"");
5857 });
5958
6059 test("jsonSchemaTypeHintFallback parenthesizes union inside intersection", () => {
@@ -100,3 +99,207 @@ test("jsonSchemaTypeHintFallback inlines small component schema refs at depth th · 39 unmodified lines
10099 expect(hint).toContain("count");
101100 expect(hint).not.toContain("components[\"schemas\"][\"Pagination\"]");
102101 });
102+
103+test("jsonSchemaTypeHintFallback drops plain scalar variants when union has multiple object shapes", () => {
104+ const schema = {
105+ oneOf: [
106+ { type: "string" },
107+ {
108+ type: "object",
109+ properties: {
110+ records: { type: "string" },
111+ },
112+ required: ["records"],
113+ },
114+ {
115+ type: "object",
116+ properties: {
117+ records: { type: "string" },
118+ pagination: { type: "number" },
119+ },
120+ required: ["records", "pagination"],
121+ },
122+ ],
123+ };
124+
125+ // string variant is pruned; remaining two objects collapse into optional pagination.
126+ expect(jsonSchemaTypeHintFallback(schema)).toBe("{ records: string; pagination?: number }");
127+});
128+
129+test("jsonSchemaTypeHintFallback drops empty object variant when a more specific object is present", () => {
130+ const schema = {
131+ oneOf: [
132+ { type: "object" },
133+ {
134+ type: "object",
135+ properties: {
136+ a: { type: "string" },
137+ },
138+ required: ["a"],
139+ },
140+ ],
141+ };
142+
143+ expect(jsonSchemaTypeHintFallback(schema)).toBe("{ a: string }");
144+});
145+
146+test("jsonSchemaTypeHintFallback keeps scalar | object when union has only one object variant", () => {
147+ const schema = {
148+ oneOf: [
149+ { type: "string" },
150+ {
151+ type: "object",
152+ properties: {
153+ a: { type: "string" },
154+ },
155+ required: ["a"],
156+ },
157+ ],
158+ };
159+
160+ expect(jsonSchemaTypeHintFallback(schema)).toBe("string | { a: string }");
161+});
162+
163+test("jsonSchemaTypeHintFallback partially factors repeated required keys from large unions", () => {
164+ const schema = {
165+ oneOf: [
166+ {
167+ type: "object",
168+ properties: {
169+ name: { type: "string" },
170+ type: { enum: ["A"] },
171+ value: { type: "string" },
172+ },
173+ required: ["name", "type", "value"],
174+ },
175+ {
176+ type: "object",
177+ properties: {
178+ name: { type: "string" },
179+ type: { enum: ["AAAA"] },
180+ value: { type: "string" },
181+ },
182+ required: ["name", "type", "value"],
183+ },
184+ {
185+ type: "object",
186+ properties: {
187+ name: { type: "string" },
188+ type: { enum: ["CNAME"] },
189+ value: { type: "string" },
190+ },
191+ required: ["name", "type"],
192+ },
193+ {
194+ type: "object",
195+ properties: {
196+ type: { enum: ["SRV"] },
197+ srv: { type: "object", properties: { target: { type: "string" } }, required: ["target"] },
198+ },
199+ required: ["type", "srv"],
200+ },
201+ ],
202+ };
203+
204+ const hint = jsonSchemaTypeHintFallback(schema);
205+ // `name` should only appear once after factoring.
206+ expect(hint.split("name: string").length - 1).toBe(1);
207+ // Still must mention SRV variant.
208+ expect(hint).toContain("type: \"SRV\"");
209+ // The factored form uses an intersection.
210+ expect(hint).toContain("& (");
211+});
212+
213+test("jsonSchemaTypeHintFallback factors common fields and then partially factors repeated keys inside residual union", () => {
214+ const schema = {
215+ anyOf: [
216+ {
217+ type: "object",
218+ properties: {
219+ name: { type: "string" },
220+ type: { enum: ["A"] },
221+ value: { type: "string" },
222+ ttl: { type: "number" },
223+ comment: { type: "string" },
224+ },
225+ required: ["name", "type", "value"],
226+ },
227+ {
228+ type: "object",
229+ properties: {
230+ name: { type: "string" },
231+ type: { enum: ["AAAA"] },
232+ value: { type: "string" },
233+ ttl: { type: "number" },
234+ comment: { type: "string" },
235+ },
236+ required: ["name", "type", "value"],
237+ },
238+ {
239+ type: "object",
240+ properties: {
241+ type: { enum: ["SRV"] },
242+ srv: {
243+ type: "object",
244+ properties: { target: { type: "string" } },
245+ required: ["target"],
246+ },
247+ ttl: { type: "number" },
248+ comment: { type: "string" },
249+ },
250+ required: ["type", "srv"],
251+ },
252+ {
253+ type: "object",
254+ properties: {
255+ name: { type: "string" },
256+ type: { enum: ["NS"] },
257+ value: { type: "string" },
258+ ttl: { type: "number" },
259+ comment: { type: "string" },
260+ },
261+ required: ["name", "type"],
262+ },
263+ ],
264+ };
265+
266+ const hint = jsonSchemaTypeHintFallback(schema);
267+ // `ttl`/`comment` appear once as common fields.
268+ expect(hint.split("ttl?: number").length - 1).toBe(1);
269+ expect(hint.split("comment?: string").length - 1).toBe(1);
270+ // `name` should also be factored once inside the residual union.
271+ expect(hint.split("name: string").length - 1).toBe(1);
272+});
273+
274+test("jsonSchemaTypeHintFallback merges discriminated object variants that only differ by type enum", () => {
275+ const schema = {
276+ oneOf: [
277+ {
278+ type: "object",
279+ properties: {
280+ name: { type: "string" },
281+ type: { type: "string", enum: ["A"] },
282+ value: { type: "string" },
283+ },
284+ required: ["name", "type", "value"],
285+ },
286+ {
287+ type: "object",
288+ properties: {
289+ name: { type: "string" },
290+ type: { type: "string", enum: ["AAAA"] },
291+ value: { type: "string" },
292+ },
293+ required: ["name", "type", "value"],
294+ },
295+ ],
296+ };
297+
298+ const hint = jsonSchemaTypeHintFallback(schema);
299+ expect(hint).toContain("name: string");
300+ expect(hint).toContain("value: string");
301+ expect(hint).toContain("type: \"A\" | \"AAAA\"");
302+ // Should not render as two separate object variants.
303+ expect(hint).not.toContain("type: \"A\"; value");
304+ expect(hint).not.toContain("type: \"AAAA\"; value");
305+});
executor/packages/core/src/openapi/schema-hints.ts+352−7
@@ -242,6 +242,179 @@ function isObjectSchema(schema: JsonSchema): boolean { · 241 unmodified lines
242242 return Object.keys(asRecord(schema.properties)).length > 0;
243243 }
244244
245+function isEmptyObjectSchema(schema: JsonSchema): boolean {
246+ const type = typeof schema.type === "string" ? schema.type : undefined;
247+ if (type !== "object") return false;
248+ return Object.keys(asRecord(schema.properties)).length === 0;
249+}
250+
251+function isPlainScalarSchema(schema: JsonSchema): boolean {
252+ const type = typeof schema.type === "string" ? schema.type : undefined;
253+ if (type !== "string" && type !== "number" && type !== "integer" && type !== "boolean") {
254+ return false;
255+ }
256+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return false;
257+
258+ // If this scalar has any meaningful constraints, keep it.
259+ const allowedKeys = new Set([
260+ "type",
261+ "title",
262+ "description",
263+ "deprecated",
264+ "examples",
265+ "example",
266+ "nullable",
267+ "default",
268+ ]);
269+ for (const key of Object.keys(schema)) {
270+ if (!allowedKeys.has(key)) return false;
271+ }
272+
273+ return true;
274+}
275+
276+function normalizeUnionSchemaVariants(variants: JsonSchema[]): JsonSchema[] {
277+ if (variants.length < 2) return variants;
278+
279+ // Many real-world OpenAPI specs include `oneOf: [{type:"object"}, { ...specific... }]`
280+ // or even `oneOf: [{type:"string"}, { ...object... }, { ...object... }]`.
281+ // These broad scalar/empty-object variants add noise without helping the UI/LLM.
282+ const hasNonEmptyObject = variants.some((v) => isObjectSchema(v) && Object.keys(asRecord(v.properties)).length > 0);
283+
284+ let filtered = variants;
285+ if (hasNonEmptyObject) {
286+ filtered = filtered.filter((v) => !isEmptyObjectSchema(v));
287+ }
288+
289+ const objectCount = filtered.filter(isObjectSchema).length;
290+ if (objectCount >= 2) {
291+ filtered = filtered.filter((v) => !isPlainScalarSchema(v));
292+ }
293+
294+ return filtered.length > 0 ? filtered : variants;
295+}
296+
297+function extractStringLiteralFromSchema(schema: JsonSchema): string | null {
298+ // Support the two common encodings we see in OpenAPI:
299+ // - { type: "string", enum: ["A"] }
300+ // - { const: "A" }
301+ const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined;
302+ if (enumValues && enumValues.length === 1 && typeof enumValues[0] === "string") {
303+ return enumValues[0];
304+ }
305+ if (typeof (schema as Record<string, unknown>).const === "string") {
306+ return String((schema as Record<string, unknown>).const);
307+ }
308+ return null;
309+}
310+
311+function mergeDiscriminatedObjectUnionVariants(
312+ variants: JsonSchema[],
313+ discriminantKey: string,
314+ depth: number,
315+ componentSchemas?: Record<string, unknown>,
316+ seenRefs: Set<string> = new Set(),
317+): JsonSchema[] {
318+ if (variants.length < 2) return variants;
319+ if (!variants.every(isObjectSchema)) return variants;
320+
321+ type VariantInfo = {
322+ idx: number;
323+ schema: JsonSchema;
324+ disc: string;
325+ signature: string;
326+ };
327+
328+ const infos: VariantInfo[] = [];
329+
330+ for (let i = 0; i < variants.length; i++) {
331+ const variant = variants[i]!;
332+ const props = asRecord(variant.properties);
333+ const discSchema = asRecord(props[discriminantKey]);
334+ const disc = extractStringLiteralFromSchema(discSchema);
335+ if (!disc) {
336+ return variants;
337+ }
338+
339+ const required = new Set(
340+ (Array.isArray(variant.required) ? variant.required : []).filter(
341+ (k): k is string => typeof k === "string",
342+ ),
343+ );
344+
345+ const otherKeys = Object.keys(props)
346+ .filter((k) => k !== discriminantKey)
347+ .sort();
348+ const requiredOther = [...required]
349+ .filter((k) => k !== discriminantKey)
350+ .sort();
351+
352+ const keyHints = otherKeys.map((k) => {
353+ const hint = jsonSchemaTypeHintFallback(props[k], depth + 1, componentSchemas, seenRefs);
354+ return `${k}:${hint}`;
355+ });
356+
357+ // Group by: same other keys + same required set + same per-key hints.
358+ const signature = JSON.stringify({ otherKeys, requiredOther, keyHints });
359+ infos.push({ idx: i, schema: variant, disc, signature });
360+ }
361+
362+ const bySig = new Map<string, VariantInfo[]>();
363+ for (const info of infos) {
364+ const bucket = bySig.get(info.signature);
365+ if (bucket) bucket.push(info);
366+ else bySig.set(info.signature, [info]);
367+ }
368+
369+ let changed = false;
370+ const used = new Set<number>();
371+ const out: JsonSchema[] = [];
372+
373+ // Preserve stable order: walk original variants and emit merged groups
374+ // when we encounter their first member.
375+ for (let i = 0; i < variants.length; i++) {
376+ if (used.has(i)) continue;
377+ const info = infos.find((x) => x.idx === i);
378+ if (!info) {
379+ out.push(variants[i]!);
380+ continue;
381+ }
382+
383+ const group = bySig.get(info.signature) ?? [info];
384+ if (group.length < 2) {
385+ out.push(variants[i]!);
386+ used.add(i);
387+ continue;
388+ }
389+
390+ // Merge group: same schema shape, only discriminant differs.
391+ changed = true;
392+ for (const member of group) used.add(member.idx);
393+
394+ const base = group[0]!.schema;
395+ const baseProps = asRecord(base.properties);
396+ const discSchema = asRecord(baseProps[discriminantKey]);
397+ const discType = typeof discSchema.type === "string" ? discSchema.type : "string";
398+
399+ const mergedDiscriminant: JsonSchema = {
400+ type: discType,
401+ enum: group.map((m) => m.disc),
402+ };
403+
404+ const mergedProps = {
405+ ...baseProps,
406+ [discriminantKey]: mergedDiscriminant,
407+ };
408+
409+ out.push({
410+ ...base,
411+ properties: mergedProps,
412+ });
413+ }
414+
415+ return changed ? out : variants;
416+}
417+
245418 function subsetKeys(left: string[], right: string[]): boolean {
246419 const rightSet = new Set(right);
247420 for (const key of left) {
@@ -373,12 +546,138 @@ function tryFactorCommonObjectFields( · 125 unmodified lines
373546 .join("; ");
374547 const commonType = `{ ${commonInner} }`;
375548
376− const residualType = joinUnion(residualSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));
549+ const partiallyFactoredResidual = tryFactorPartialCommonObjectFields(
550+ residualSchemas,
551+ depth,
552+ componentSchemas,
553+ seenRefs,
554+ );
555+
556+ const residualType = partiallyFactoredResidual
557+ ?? joinUnion(residualSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));
377558 if (residualType === "unknown") return commonType;
378559
379560 return `${commonType} & (${residualType})`;
380561 }
381562
563+function tryFactorPartialCommonObjectFields(
564+ variants: JsonSchema[],
565+ depth: number,
566+ componentSchemas?: Record<string, unknown>,
567+ seenRefs: Set<string> = new Set(),
568+): string | null {
569+ // If there's a large union where a field repeats across many (but not all)
570+ // object variants (e.g. Vercel DNS createRecord repeats `name` in most
571+ // variants), pull it out as:
572+ // ({ name: string } & (<union-without-name>)) | <remaining>
573+ // This is purely a *hint* readability improvement.
574+
575+ if (variants.length < 3) return null;
576+ if (!variants.every(isObjectSchema)) return null;
577+
578+ const propsList = variants.map((v) => asRecord(v.properties));
579+ const requiredList = variants.map((v) => new Set(
580+ (Array.isArray(v.required) ? v.required : []).filter((k): k is string => typeof k === "string"),
581+ ));
582+
583+ type Candidate = {
584+ key: string;
585+ hint: string;
586+ indices: number[];
587+ };
588+
589+ const candidates: Candidate[] = [];
590+ const minGroupSize = variants.length >= 4 ? 3 : 2;
591+
592+ // Collect keys present in at least minGroupSize variants.
593+ const keyCounts = new Map<string, number>();
594+ for (const props of propsList) {
595+ for (const key of Object.keys(props)) {
596+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
597+ }
598+ }
599+
600+ for (const [key, count] of keyCounts.entries()) {
601+ if (count < minGroupSize) continue;
602+
603+ // Consider only keys that are required in the variants we factor.
604+ const indicesWithKey = propsList
605+ .map((props, idx) => (props[key] !== undefined ? idx : -1))
606+ .filter((idx) => idx !== -1);
607+
608+ // Group by identical type hint.
609+ const byHint = new Map<string, number[]>();
610+ for (const idx of indicesWithKey) {
611+ const variant = variants[idx]!;
612+ if (!requiredList[idx]!.has(key)) continue;
613+ const props = propsList[idx]!;
614+ const hint = jsonSchemaTypeHintFallback(props[key], depth + 1, componentSchemas, seenRefs);
615+ const bucket = byHint.get(hint);
616+ if (bucket) bucket.push(idx);
617+ else byHint.set(hint, [idx]);
618+ }
619+
620+ for (const [hint, indices] of byHint.entries()) {
621+ if (indices.length < minGroupSize) continue;
622+ if (indices.length === variants.length) continue;
623+ candidates.push({ key, hint, indices });
624+ }
625+ }
626+
627+ if (candidates.length === 0) return null;
628+
629+ // Pick the best candidate: largest group first, then stable key order.
630+ candidates.sort((a, b) => {
631+ if (b.indices.length !== a.indices.length) return b.indices.length - a.indices.length;
632+ if (a.key !== b.key) return a.key.localeCompare(b.key);
633+ return a.hint.localeCompare(b.hint);
634+ });
635+ const best = candidates[0]!;
636+
637+ // Build the factored group.
638+ const commonType = `{ ${formatTsPropertyKey(best.key)}: ${best.hint} }`;
639+ const bestSet = new Set(best.indices);
640+
641+ const residualSchemas: JsonSchema[] = [];
642+ const remainingSchemas: JsonSchema[] = [];
643+
644+ for (let i = 0; i < variants.length; i++) {
645+ const variant = variants[i]!;
646+ const props = propsList[i]!;
647+ if (!bestSet.has(i)) {
648+ remainingSchemas.push(variant);
649+ continue;
650+ }
651+
652+ const residualProps = Object.fromEntries(
653+ Object.entries(props).filter(([k]) => k !== best.key),
654+ );
655+ const req = Array.isArray(variant.required) ? variant.required : [];
656+ const residualRequired = req.filter(
657+ (k) => typeof k === "string" && k !== best.key,
658+ );
659+
660+ residualSchemas.push({
661+ type: "object",
662+ properties: residualProps,
663+ ...(residualRequired.length > 0 ? { required: residualRequired } : {}),
664+ } as unknown as JsonSchema);
665+ }
666+
667+ // If factoring doesn't actually remove anything useful, bail.
668+ const anyResidualHasProps = residualSchemas.some(
669+ (s) => Object.keys(asRecord(s.properties)).length > 0,
670+ );
671+ if (!anyResidualHasProps) return null;
672+
673+ const residualType = joinUnion(residualSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));
674+ const groupType = `${commonType} & (${residualType})`;
675+
676+ if (remainingSchemas.length === 0) return groupType;
677+ const remainingType = joinUnion(remainingSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));
678+ return joinUnion([groupType, remainingType]);
679+}
680+
382681 function joinIntersection(parts: string[]): string {
383682 const expanded = parts.flatMap((part) => splitTopLevelBy(part, "&"));
384683 const unique = dedupeTypeParts(expanded).filter((part) => part !== "unknown");
@@ -431,8 +730,23 @@ export function jsonSchemaTypeHintFallback( · 46 unmodified lines
431730
432731 const oneOf = Array.isArray(shape.oneOf) ? shape.oneOf : undefined;
433732 if (oneOf && oneOf.length > 0) {
434− const collapsed = tryCollapseSimpleObjectUnion(
733+ const variants = normalizeUnionSchemaVariants(
435734 oneOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),
735+ );
736+ const objectVariantsMerged = mergeDiscriminatedObjectUnionVariants(
737+ variants.filter(isObjectSchema),
738+ "type",
739+ depth,
740+ componentSchemas,
741+ seenRefs,
742+ );
743+ const mergedVariants = variants.some((v) => !isObjectSchema(v))
744+ ? variants.filter((v) => !isObjectSchema(v)).concat(objectVariantsMerged)
745+ : objectVariantsMerged;
746+ const objectVariants = mergedVariants.filter(isObjectSchema);
747+
748+ const collapsed = tryCollapseSimpleObjectUnion(
749+ objectVariants,
436750 depth,
437751 componentSchemas,
438752 seenRefs,
@@ -440,19 +754,42 @@ export function jsonSchemaTypeHintFallback( · 1 unmodified line
440754 if (collapsed) return collapsed;
441755
442756 const factored = tryFactorCommonObjectFields(
443− oneOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),
757+ objectVariants,
444758 depth,
445759 componentSchemas,
446760 seenRefs,
447761 );
448762 if (factored) return factored;
449− return joinUnion(oneOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));
763+
764+ const partial = tryFactorPartialCommonObjectFields(
765+ objectVariants,
766+ depth,
767+ componentSchemas,
768+ seenRefs,
769+ );
770+ if (partial) return partial;
771+ return joinUnion(mergedVariants.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));
450772 }
451773
452774 const anyOf = Array.isArray(shape.anyOf) ? shape.anyOf : undefined;
453775 if (anyOf && anyOf.length > 0) {
454− const collapsed = tryCollapseSimpleObjectUnion(
776+ const variants = normalizeUnionSchemaVariants(
455777 anyOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),
778+ );
779+ const objectVariantsMerged = mergeDiscriminatedObjectUnionVariants(
780+ variants.filter(isObjectSchema),
781+ "type",
782+ depth,
783+ componentSchemas,
784+ seenRefs,
785+ );
786+ const mergedVariants = variants.some((v) => !isObjectSchema(v))
787+ ? variants.filter((v) => !isObjectSchema(v)).concat(objectVariantsMerged)
788+ : objectVariantsMerged;
789+ const objectVariants = mergedVariants.filter(isObjectSchema);
790+
791+ const collapsed = tryCollapseSimpleObjectUnion(
792+ objectVariants,
456793 depth,
457794 componentSchemas,
458795 seenRefs,
@@ -460,13 +797,21 @@ export function jsonSchemaTypeHintFallback( · 1 unmodified line
460797 if (collapsed) return collapsed;
461798
462799 const factored = tryFactorCommonObjectFields(
463− anyOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),
800+ objectVariants,
464801 depth,
465802 componentSchemas,
466803 seenRefs,
467804 );
468805 if (factored) return factored;
469− return joinUnion(anyOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));
806+
807+ const partial = tryFactorPartialCommonObjectFields(
808+ objectVariants,
809+ depth,
810+ componentSchemas,
811+ seenRefs,
812+ );
813+ if (partial) return partial;
814+ return joinUnion(mergedVariants.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));
470815 }
471816
472817 const allOf = Array.isArray(shape.allOf) ? shape.allOf : undefined;