Skip to content

Commit a8b42d7

Browse files
committed
Enhance schema inference for arrays of objects by merging properties and tracking required fields; update JSON schema URL in generated output
1 parent e0ada87 commit a8b42d7

File tree

2 files changed

+61
-9
lines changed

2 files changed

+61
-9
lines changed

src/lib/schema-inference.ts

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,56 @@ export function inferSchema(obj: unknown): JSONSchema {
4343
);
4444

4545
if (allSameType && itemSchemas.length > 0) {
46-
// For consistent arrays, use the first item's schema
46+
if (asObjectSchema(itemSchemas[0]).type === "object") {
47+
// For arrays of objects, merge all properties
48+
const properties: Record<string, JSONSchema> = {};
49+
const requiredProps = new Set<string>();
50+
const seenProps = new Set<string>();
51+
52+
for (const schema of itemSchemas) {
53+
const objSchema = asObjectSchema(schema);
54+
if (!objSchema.properties) continue;
55+
56+
// Track which properties we've seen
57+
for (const [key, value] of Object.entries(objSchema.properties)) {
58+
if (!seenProps.has(key)) {
59+
properties[key] = value;
60+
seenProps.add(key);
61+
}
62+
}
63+
64+
// Track which properties are in all objects
65+
if (objSchema.required) {
66+
if (requiredProps.size === 0) {
67+
for (const prop of objSchema.required) {
68+
requiredProps.add(prop);
69+
}
70+
} else {
71+
const newRequired = new Set<string>();
72+
for (const prop of objSchema.required) {
73+
if (requiredProps.has(prop)) newRequired.add(prop);
74+
}
75+
requiredProps.clear();
76+
for (const prop of newRequired) {
77+
requiredProps.add(prop);
78+
}
79+
}
80+
}
81+
}
82+
83+
return {
84+
type: "array",
85+
items: {
86+
type: "object",
87+
properties,
88+
required:
89+
requiredProps.size > 0 ? Array.from(requiredProps) : undefined,
90+
},
91+
minItems: 0,
92+
};
93+
}
94+
95+
// For other consistent arrays, use the first item's schema
4796
return {
4897
type: "array",
4998
items: itemSchemas[0],
@@ -53,15 +102,15 @@ export function inferSchema(obj: unknown): JSONSchema {
53102

54103
// For mixed type arrays, create a oneOf schema
55104
if (itemSchemas.length > 0) {
105+
const uniqueSchemas = [
106+
...new Map(itemSchemas.map((s) => [JSON.stringify(s), s])).values(),
107+
];
56108
return {
57109
type: "array",
58-
items: {
59-
oneOf: [
60-
...new Map(
61-
itemSchemas.map((s) => [JSON.stringify(s), s]),
62-
).values(),
63-
],
64-
},
110+
items:
111+
uniqueSchemas.length === 1
112+
? uniqueSchemas[0]
113+
: { oneOf: uniqueSchemas },
65114
minItems: 0,
66115
};
67116
}
@@ -112,7 +161,7 @@ export function createSchemaFromJson(jsonObject: unknown): JSONSchema {
112161
const inferredSchema = inferSchema(jsonObject);
113162

114163
return {
115-
$schema: "http://json-schema.org/draft-07/schema#",
164+
$schema: "https://json-schema.org/draft-07/schema",
116165
...asObjectSchema(inferredSchema),
117166
title: "Generated Schema",
118167
description: "Generated from JSON data",

test/schemaInference.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ test("should infer schema for array of objects with different properties", () =>
8181
schema.properties.users.items.properties.address.type,
8282
"string",
8383
);
84+
// "name" is present in all objects, so it is required;
85+
// "address" is present in some objects, so it is not required;
86+
assert.deepStrictEqual(schema.properties.users.items.required, ["name"]);
8487
});
8588

8689
test("should detect string formats", () => {

0 commit comments

Comments
 (0)