postprocess-openapi: also fix type: Any and duplicate params

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) <noreply@anthropic.com>
This commit is contained in:
Uwe Schuster 2026-05-05 21:25:49 +02:00
parent 4c4d52e3de
commit 8677faf54b
2 changed files with 48 additions and 5 deletions

View file

@ -48,12 +48,55 @@ for path, methods in paths.items():
# breaks strict consumers like Orval's Zod generator. Strip empty arrays — # breaks strict consumers like Orval's Zod generator. Strip empty arrays —
# absence of the keyword has the same semantics. # absence of the keyword has the same semantics.
schemas = spec.get("components", {}).get("schemas", {}) schemas = spec.get("components", {}).get("schemas", {})
stripped = 0 stripped_required = 0
for sch in schemas.values(): for sch in schemas.values():
if isinstance(sch, dict) and sch.get("required") == []: if isinstance(sch, dict) and sch.get("required") == []:
del sch["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)) SRC.write_text(json.dumps(spec, indent=2))
print(f" postprocessed {SRC}{len(paths)} paths" report = [f"{len(paths)} paths"]
+ (f", stripped empty `required` from {stripped} schemas" if stripped else "")) 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))

View file

@ -1,6 +1,6 @@
{ {
"name": "@uschuster/webapp-scaffold", "name": "@uschuster/webapp-scaffold",
"version": "0.4.1", "version": "0.4.2",
"description": "Shared build scripts + Vite config factories for webapp-template-derived projects.", "description": "Shared build scripts + Vite config factories for webapp-template-derived projects.",
"type": "module", "type": "module",
"bin": { "bin": {