Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1504,12 +1504,30 @@ function withNullType(schema: JSONSchema7): JSONSchema7 {
* If any segment along the path is optional, the entire expression
* can produce `null`/`undefined` at runtime.
*
* Intrinsic array accesses (`.length`, `.[N]`) are always considered
* required — they are synthesized by the schema resolver and do not
* appear in any `required` array.
*
* Example: for path `["user", "name"]`, checks both that `user` is
* required in the root schema AND that `name` is required in `user`.
*/
function isPathFullyRequired(schema: JSONSchema7, segments: string[]): boolean {
for (let i = 1; i <= segments.length; i++) {
if (!isPropertyRequired(schema, segments.slice(0, i))) return false;
if (!isPropertyRequired(schema, segments.slice(0, i))) {
// Intrinsic array access: .length and .[N] are always available
// on array schemas. They are not listed in `required` because
// they are synthesized by the schema resolver, not real properties.
if (i >= 2) {
const parentSchema = resolveSchemaPath(
schema,
segments.slice(0, i - 1),
);
if (parentSchema && parentSchema.type === "array") {
continue;
}
}
return false;
}
}
return true;
}
Expand Down
54 changes: 54 additions & 0 deletions tests/analyzer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,60 @@ describe("analyzer", () => {
required: ["l1"],
});
});

// ── Intrinsic array access (.length, .[N]) ───────────────────────

test(".length on required array → integer (not nullable)", () => {
const schema: JSONSchema7 = {
type: "object",
properties: {
items: { type: "array", items: { type: "string" } },
},
required: ["items"],
};
const result = analyze("{{items.length}}", schema);
expect(result.valid).toBe(true);
expect(result.outputSchema).toEqual({ type: "integer" });
});

test(".[0] on required array → element type (not nullable)", () => {
const schema: JSONSchema7 = {
type: "object",
properties: {
items: { type: "array", items: { type: "number" } },
},
required: ["items"],
};
const result = analyze("{{items.[0]}}", schema);
expect(result.valid).toBe(true);
expect(result.outputSchema).toEqual({ type: "number" });
});

test(".length on optional array → nullable integer", () => {
const schema: JSONSchema7 = {
type: "object",
properties: {
items: { type: "array", items: { type: "string" } },
},
// items is NOT required
};
const result = analyze("{{items.length}}", schema);
expect(result.valid).toBe(true);
expect(result.outputSchema).toEqual({ type: ["integer", "null"] });
});

test(".[0] on optional array → nullable element type", () => {
const schema: JSONSchema7 = {
type: "object",
properties: {
items: { type: "array", items: { type: "number" } },
},
// items is NOT required
};
const result = analyze("{{items.[0]}}", schema);
expect(result.valid).toBe(true);
expect(result.outputSchema).toEqual({ type: ["number", "null"] });
});
});

describe("diagnostics include a position (loc)", () => {
Expand Down
Loading