When Codegen Ignores format: uuid, email, and uri and Gives You a Plain String
Quick answer: OpenAPI format hints such as uuid, email, uri, and date-time are annotations, not enforced types, and most generators emit a plain string for them unless you configure a specific type mapping. That means the compiler will not catch a malformed identifier or a bad URL. Treat the format as documentation, add explicit validation or a branded type at the boundary, and do not assume the generated client verifies the value.
Why format collapses into string
The JSON Schema spec keeps type and format separate on purpose: type defines the structural data type, while format adds a semantic hint that is not guaranteed to be enforced. Generators therefore map the structural type and leave the hint to the runtime, which often ignores it. A field declared as type: string, format: uuid becomes a TypeScript string, and any string value will type-check even if it is not a valid UUID.
| OpenAPI hint | Typical generated type | What is actually checked |
|---|---|---|
| format: uuid | string | Nothing at compile time |
| format: email | string | Nothing at compile time |
| format: uri | string | Nothing at compile time |
| format: date-time | string (or Date with config) | Depends on generator mapping |
The false confidence
Consider a generated update function that takes a user ID. Because the type is just string, a typo or a value taken from the wrong field compiles without complaint and fails only when the server rejects it.
// Generated type: the uuid hint is already gone
type UpdateUser = {
userId: string;
displayName: string;
};
// This compiles even though it is not a valid UUID
const update: UpdateUser = {
userId: "not-a-uuid",
displayName: "Ada"
};
The failure is pushed to the network round trip instead of being caught at the editor, which is exactly where a semantic hint was meant to help.
Option 1: validate at the boundary
One reliable approach is to keep the generated string but validate untrusted values before they enter the system, using a small parser or a schema library. This converts an unchecked string into a confirmed value and gives a clear error for bad input.
function asUuid(value: string): string {
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
throw new Error(`invalid UUID: ${value}`);
}
return value;
}
const update = {
userId: asUuid(input.userId),
displayName: input.displayName
};
Option 2: branded types for compile-time distinction
If you want the editor to distinguish a UUID from an ordinary string, wrap the value in a branded type that can only be produced by the validator. This keeps a malformed value from being passed silently, while the underlying wire format stays a string.
type Uuid = string & { readonly __brand: "Uuid" }; function asUuid(value: string): Uuid { if (!/^[0-9a-f-]{36}$/i.test(value)) throw new Error("invalid UUID"); return value as Uuid; } function updateUser(userId: Uuid, displayName: string) { return client.put("/users", { userId, displayName }); }Configuring generator type mappings
Many generators accept a mapping from format to a language type, and some map
date-timeto a nativeDateautomatically. Review the generated output rather than assuming the hint is honored, and set the mapping explicitly when you need it. Even with a mapped type, remember that deserialization still arrives as a string over the wire, so the runtime parse remains the point where a malformed value can actually be rejected.How an agent misreads it
An agent that sees
format: uuidoften assumes the generated type enforces UUID validity and skips validation, then a malformed value slips through. When reviewing generated code, ask the agent to show the actual emitted type and identify where the format is checked; if the answer is nowhere, add a boundary validator. If the agent simply adds a regex in every handler, consolidate it into one reusable parser so the rule stays consistent across the codebase.Validating URIs and emails at the same boundary
UUIDs are the easiest format to check with a pattern, but URIs and emails need a little more care because their shape is broader. The built-in URL constructor is a reliable parser for URIs and throws on malformed input, while email is best handled with a deliberately conservative check rather than an elaborate regex that tries to encode the entire address specification. In both cases, the goal is the same as for UUIDs: parse once at the boundary and keep a confirmed value for the rest of the call.
function asUrl(value: string): URL { try { return new URL(value); } catch { throw new Error(`invalid URI: ${value}`); } } function asEmail(value: string): string { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new Error(`invalid email: ${value}`); } return value; } const homepage = asUrl(input.homepage).toString(); const email = asEmail(input.contactEmail);Keeping these parsers alongside the UUID validator in one module means every format hint in the spec has a matching runtime check, and new code reaches for the existing helper instead of inventing a fresh regex. It also gives you a natural place to tighten a rule later; a change in one parser immediately applies everywhere the format is consumed.
Deciding between validation and branding
Plain validation is enough when the value is used close to where it enters the system, but a branded type earns its keep when the confirmed value travels across several modules, because it prevents an unvalidated string from being reintroduced later. The two approaches compose well: the parser produces the branded value, and the type system carries that confidence through the rest of the code. Pick the lightest option that still keeps a malformed value from reaching the wire, and avoid wrapping types that never leave the boundary.
Frequently asked questions
Does format: uuid guarantee a UUID type in the client?
No. Most generators emit a plain string because format is a semantic hint rather than a structural type, so the compiler will not validate the value.
How can I enforce the format myself?
Validate at the boundary with a parser or schema library, or use a branded type that can only be created after validation, so malformed values cannot enter the rest of the system.
What about date-time formats?
Some generators map date-time to a native Date when configured, but the value still arrives as a string over the wire, so confirm the runtime parse handles invalid or timezone-suffixed input.
Is format useless then?
Not at all. It documents intent, drives documentation and tooling, and can be wired to explicit type mappings or validators; it simply does not enforce anything on its own.
This is part of the The Code Generation Guide — start there for the full picture of what generated clients do and do not guarantee.