From 15ea876601724d07fb85ae0fcd05f6a1135f3667 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sat, 2 May 2026 14:52:42 -0500 Subject: [PATCH] fix(@effect/platform): flatten anyOf when accumulating same-content-type union payloads OpenApi.fromApi was emitting nested anyOf arrays for flat unions passed to setPayload when all members share the same content type. Each new member was wrapped via AST.Union.make([current, next]), creating left-nested Union nodes that the JSON Schema encoder preserved as nested anyOf. The fix replaces that call with HttpApiSchema.UnionUnifyAST, which already existed and is already used by the response-map accumulator for the same purpose. It flattens both sides via extractUnionTypes before creating the final Union node. Fixes #6052 --- .../fix-platform-flat-anyof-union-payload.md | 5 ++ packages/platform/src/HttpApi.ts | 2 +- packages/platform/test/OpenApi.test.ts | 72 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-platform-flat-anyof-union-payload.md diff --git a/.changeset/fix-platform-flat-anyof-union-payload.md b/.changeset/fix-platform-flat-anyof-union-payload.md new file mode 100644 index 00000000000..7416144aee1 --- /dev/null +++ b/.changeset/fix-platform-flat-anyof-union-payload.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": patch +--- + +`OpenApi.fromApi` now emits a flat `anyOf` array when multiple union members in `setPayload` share the same content type. Previously, each additional member was wrapped in a new `Union` node, producing deeply nested `anyOf: [ anyOf: [A, B], C ]` structures. The fix uses `HttpApiSchema.UnionUnifyAST` — already used by the response-map accumulator — to flatten unions as they are collected. diff --git a/packages/platform/src/HttpApi.ts b/packages/platform/src/HttpApi.ts index 2cbcf250648..c7f981d3d2d 100644 --- a/packages/platform/src/HttpApi.ts +++ b/packages/platform/src/HttpApi.ts @@ -419,7 +419,7 @@ const extractPayloads = (topAst: AST.AST): ReadonlyMap { expectSpecPaths(api, expected) }) }) + + it("setPayload with a flat JSON union produces flat anyOf (not nested)", () => { + const A = Schema.Struct({ _tag: Schema.Literal("A"), a: Schema.String }) + const B = Schema.Struct({ _tag: Schema.Literal("B"), b: Schema.Number }) + const C = Schema.Struct({ _tag: Schema.Literal("C"), c: Schema.Boolean }) + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("post", "/") + .addSuccess(Schema.String) + .setPayload(Schema.Union(A, B, C)) + ) + ) + expectSpecPaths(api, { + "/": { + "post": { + "tags": ["group"], + "operationId": "group.post", + "parameters": [], + "security": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": ["_tag", "a"], + "properties": { + "_tag": { "enum": ["A"], "type": "string" }, + "a": { "type": "string" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["_tag", "b"], + "properties": { + "_tag": { "enum": ["B"], "type": "string" }, + "b": { "type": "number" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["_tag", "c"], + "properties": { + "_tag": { "enum": ["C"], "type": "string" }, + "c": { "type": "boolean" } + }, + "additionalProperties": false + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { "type": "string" } + } + } + }, + "400": HttpApiDecodeError + } + } + } + }) + }) }) describe("HttpApiEndpoint.del", () => {