π¦«ποΈ dispatch to foreman
π§ task enqueued
ββ priority = ?
ββ yieldage = ?
ββ leverage = ?
title
fix(introspection): z.custom 500s the debug surface β pass unrepresentable: 'any' (distinct root from #17)
description
.what
the introspection middleware renders a handler's schema with zod's default
unrepresentable: 'throw'. any schema that carries a z.custom therefore 500s the introspection
route.
ask: pass unrepresentable: 'any'. a schema that cannot be rendered should degrade to any in a
debug surface, never crash it.
.why
on ahbode/svc-gateway, introspection 500s at prep on all 75 rest handlers β the whole debug
surface, and the failure lands at the one moment a developer reaches for it.
the mechanism is one option:
z.toJSONSchema(schema) // default: unrepresentable: 'throw'
z.toJSONSchema(schema, { unrepresentable: 'any' }) // degrades instead
z.custom has no json-schema rendition, by construction. so any consumer whose domain carries a
branded or custom-validated type β a uuid brand, an iso-price words type, a domain-object hydrator β
hits this the moment introspection is enabled. that is not an edge case; it is the ordinary shape of
a typed domain.
.the severity argument, which is the whole point
introspection is a diagnostic. its entire value is to be reachable when the rest of the service is
not. a diagnostic that throws on a legitimate input is worse than an absent one, because:
- it reads as an outage of the service rather than a limit of the renderer
- it fails service-wide, not per-handler β one
z.custom anywhere in a handler's schema takes the
route down for that handler, and a shared domain type takes down all of them
- the thrown error names zod's internals, not the schema key at fault, so the 500 says no word about
which type could not be rendered (rule.require.errors-name-the-fix)
'any' is the correct default for this surface specifically. a {} in the rendered schema is a true
statement β this input is not describable in json-schema β and it keeps the other 40 keys legible.
.the ask, in preference order
unrepresentable: 'any' in the introspection middleware. one option, and it closes the class
- better, name what degraded: emit the key path alongside, so a reader of the rendered schema can
see which field fell back to any rather than guess
- if a consumer genuinely wants the strict behavior, expose it as an opt-in β but the safe path
should be the default one (rule.require.safe-by-default)
.not a duplicate of #17
#17 is "introspection crashes on an input schema that carries a .transform()". adjacent, and a
different root:
|
#17 |
this |
| the input at fault |
a .transform() |
a z.custom |
| the mechanism |
the transform branch of the renderer |
zod's unrepresentable default |
| the fix |
handle the transform |
pass one option |
a fix to either leaves the other live. they are worth a shared acceptance test β a schema that
carries both β but they are two repairs.
.acceptance
.provenance and bounds
verified: the 500 at prep across the rest surface, observed; that our half of the repair is
complete and must not change; and that z.custom is the trigger.
not verified: whether unrepresentable: 'any' alone also resolves #17's transform case β the
transform branch of the renderer was not read, so the two are filed apart rather than merged on a
guess.
.reference
ahbode/svc-gateway β .route/v2026_08_11.declapract.upgrade/3.2.reflect.test.defects.yield.md Β§D22.
π¦«ποΈ dispatch to foreman
title
fix(introspection): z.custom 500s the debug surface β pass unrepresentable: 'any' (distinct root from #17)
description
.what
the introspection middleware renders a handler's schema with zod's default
unrepresentable: 'throw'. any schema that carries az.customtherefore 500s the introspectionroute.
ask: pass
unrepresentable: 'any'. a schema that cannot be rendered should degrade toanyin adebug surface, never crash it.
.why
on
ahbode/svc-gateway, introspection 500s at prep on all 75 rest handlers β the whole debugsurface, and the failure lands at the one moment a developer reaches for it.
the mechanism is one option:
z.customhas no json-schema rendition, by construction. so any consumer whose domain carries abranded or custom-validated type β a uuid brand, an iso-price words type, a domain-object hydrator β
hits this the moment introspection is enabled. that is not an edge case; it is the ordinary shape of
a typed domain.
.the severity argument, which is the whole point
introspection is a diagnostic. its entire value is to be reachable when the rest of the service is
not. a diagnostic that throws on a legitimate input is worse than an absent one, because:
z.customanywhere in a handler's schema takes theroute down for that handler, and a shared domain type takes down all of them
which type could not be rendered (
rule.require.errors-name-the-fix)'any'is the correct default for this surface specifically. a{}in the rendered schema is a truestatement β this input is not describable in json-schema β and it keeps the other 40 keys legible.
.the ask, in preference order
unrepresentable: 'any'in the introspection middleware. one option, and it closes the classsee which field fell back to
anyrather than guessshould be the default one (
rule.require.safe-by-default).not a duplicate of #17
#17 is "introspection crashes on an input schema that carries a
.transform()". adjacent, and adifferent root:
.transform()z.customunrepresentabledefaulta fix to either leaves the other live. they are worth a shared acceptance test β a schema that
carries both β but they are two repairs.
.acceptance
z.customcan be introspected with no crash{}with no tellz.customalone,.transform()alone (ποΈ task - fix: introspection crashes on an input schema that carries a transformΒ #17), and both together.the gap in each case exists because they are only ever exercised apart
.provenance and bounds
verified: the 500 at prep across the rest surface, observed; that our half of the repair is
complete and must not change; and that
z.customis the trigger.not verified: whether
unrepresentable: 'any'alone also resolves #17's transform case β thetransform branch of the renderer was not read, so the two are filed apart rather than merged on a
guess.
.reference
ahbode/svc-gatewayβ.route/v2026_08_11.declapract.upgrade/3.2.reflect.test.defects.yield.mdΒ§D22.