From 2faf1c891db6f15b412751a4f4ed66dda6e84622 Mon Sep 17 00:00:00 2001 From: airdropzamani Date: Mon, 31 Aug 2026 03:18:05 +0300 Subject: [PATCH] fix(agent): handle nullable JSON Schema type unions in skeletons Normalize array-form type before building workflow input skeletons. Adds regression tests. Fixes #614. Co-authored-by: Cursor --- .../introspection-nullable-type-union.md | 8 ++++ packages/agent/src/introspection.spec.ts | 44 +++++++++++++++++++ packages/agent/src/introspection.ts | 6 ++- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .changeset/introspection-nullable-type-union.md create mode 100644 packages/agent/src/introspection.spec.ts diff --git a/.changeset/introspection-nullable-type-union.md b/.changeset/introspection-nullable-type-union.md new file mode 100644 index 000000000..a2389c2c4 --- /dev/null +++ b/.changeset/introspection-nullable-type-union.md @@ -0,0 +1,8 @@ +--- +"@sapiom/agent": patch +--- + +Fix `exampleFromJsonSchema` skeleton generation for JSON Schema `type` unions +that include `null` (e.g. `{ type: ["string", "null"] }` from Zod `.nullable()`). +Previously the array form fell through to `null`, so workflow input prefills +showed `null` instead of a type-appropriate placeholder. diff --git a/packages/agent/src/introspection.spec.ts b/packages/agent/src/introspection.spec.ts new file mode 100644 index 000000000..f5c3f738c --- /dev/null +++ b/packages/agent/src/introspection.spec.ts @@ -0,0 +1,44 @@ +import { exampleFromJsonSchema } from "./introspection.js"; + +describe("exampleFromJsonSchema", () => { + it("prefers an author-declared example", () => { + expect( + exampleFromJsonSchema({ + type: "string", + examples: ["from-author"], + }), + ).toBe("from-author"); + }); + + it("builds a string skeleton for nullable string types (type union array)", () => { + expect(exampleFromJsonSchema({ type: ["string", "null"] })).toBe(""); + }); + + it("builds a number skeleton for nullable number types", () => { + expect(exampleFromJsonSchema({ type: ["number", "null"] })).toBe(0); + }); + + it("builds an integer skeleton for nullable integer types", () => { + expect(exampleFromJsonSchema({ type: ["integer", "null"] })).toBe(0); + }); + + it("builds a boolean skeleton for nullable boolean types", () => { + expect(exampleFromJsonSchema({ type: ["boolean", "null"] })).toBe(false); + }); + + it("returns null when null is the only type in the union", () => { + expect(exampleFromJsonSchema({ type: ["null"] })).toBeNull(); + }); + + it("recurses into object properties with nullable scalar fields", () => { + expect( + exampleFromJsonSchema({ + type: "object", + properties: { + name: { type: ["string", "null"] }, + count: { type: ["integer", "null"] }, + }, + }), + ).toEqual({ name: "", count: 0 }); + }); +}); diff --git a/packages/agent/src/introspection.ts b/packages/agent/src/introspection.ts index a5c12f690..eae7585c9 100644 --- a/packages/agent/src/introspection.ts +++ b/packages/agent/src/introspection.ts @@ -105,7 +105,11 @@ function skeletonFromJsonSchema(schema: Record): unknown { return skeletonFromJsonSchema(branches[0]); } - switch (schema.type) { + const type = Array.isArray(schema.type) + ? (schema.type as string[]).find((t) => t !== "null") ?? "null" + : schema.type; + + switch (type) { case 'object': { const props = (schema.properties ?? {}) as Record>; const out: Record = {};