From 8677faf54b384ebc67e2e3da275f54dbcd773e36 Mon Sep 17 00:00:00 2001 From: Uwe Schuster Date: Tue, 5 May 2026 21:25:49 +0200 Subject: [PATCH] postprocess-openapi: also fix `type: Any` and duplicate params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more oatpp 1.3 spec quirks surfaced when fewo-webapp's #469 increment 2 wired up Orval's strict Zod codegen on top of the v0.4.1 `required: []` strip: - `"type": "Any"` is emitted for `oatpp::Any` / `oatpp::Fields` fields. OpenAPI 3.0 has no "Any" type — the empty schema `{}` (or absence of `type`) is the canonical "any value" form. Strip the offending key recursively across the whole spec. - Some endpoints emit the same path parameter twice (same `name` + same `in: path`). Dedupe by `(name, in)` preserving first occurrence. These were caught only by `@scalar/openapi-parser`'s strict mode (used by Orval's Zod project); the fetch-client project tolerated them. Without both fixes, fewoZod fails with `must NOT have duplicate items` and `must match exactly one schema in oneOf` per affected component. Bumps to v0.4.2. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/postprocess-openapi.py | 51 +++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/bin/postprocess-openapi.py b/bin/postprocess-openapi.py index 5a6a966..8025af0 100755 --- a/bin/postprocess-openapi.py +++ b/bin/postprocess-openapi.py @@ -48,12 +48,55 @@ for path, methods in paths.items(): # breaks strict consumers like Orval's Zod generator. Strip empty arrays — # absence of the keyword has the same semantics. schemas = spec.get("components", {}).get("schemas", {}) -stripped = 0 +stripped_required = 0 for sch in schemas.values(): if isinstance(sch, dict) and sch.get("required") == []: del sch["required"] - stripped += 1 + stripped_required += 1 + +# oatpp 1.3 emits `"type": "Any"` for `oatpp::Any` / `oatpp::Fields` fields. +# `Any` is not a valid OpenAPI 3.0 type — the spec uses an empty schema `{}` +# (or `nullable: true` with no type) to mean "any value". Replace `Any` with +# an empty schema; recurse so we catch nested cases (object property, +# array item, etc.). +fixed_any = 0 +def fix_any(node): + global fixed_any + if isinstance(node, dict): + if node.get("type") == "Any": + del node["type"] + fixed_any += 1 + for v in node.values(): + fix_any(v) + elif isinstance(node, list): + for item in node: + fix_any(item) +fix_any(spec) + +# oatpp 1.3 sometimes emits the same path parameter twice (same name + same +# `in: path`). OpenAPI 3.0 rejects duplicate parameters. Dedupe by (name, in) +# preserving first occurrence. +deduped_params = 0 +for path_item in paths.values(): + if not isinstance(path_item, dict): + continue + for op in path_item.values(): + if not isinstance(op, dict) or "parameters" not in op: + continue + seen = set() + unique = [] + for p in op["parameters"]: + key = (p.get("name"), p.get("in")) + if key in seen: + deduped_params += 1 + continue + seen.add(key) + unique.append(p) + op["parameters"] = unique SRC.write_text(json.dumps(spec, indent=2)) -print(f" postprocessed {SRC} — {len(paths)} paths" - + (f", stripped empty `required` from {stripped} schemas" if stripped else "")) +report = [f"{len(paths)} paths"] +if stripped_required: report.append(f"stripped empty `required` from {stripped_required} schemas") +if fixed_any: report.append(f"replaced {fixed_any} invalid `type: Any` with any-schema") +if deduped_params: report.append(f"deduped {deduped_params} duplicate parameter(s)") +print(f" postprocessed {SRC} — " + ", ".join(report)) diff --git a/package.json b/package.json index ad527b1..efa5653 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uschuster/webapp-scaffold", - "version": "0.4.1", + "version": "0.4.2", "description": "Shared build scripts + Vite config factories for webapp-template-derived projects.", "type": "module", "bin": {