Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user handling of format strings in OpenAPI #1859

Open
ntjess opened this issue Mar 25, 2025 · 8 comments
Open

Allow user handling of format strings in OpenAPI #1859

ntjess opened this issue Mar 25, 2025 · 8 comments
Labels
feature 🚀 New feature or request

Comments

@ntjess
Copy link

ntjess commented Mar 25, 2025

Description

From OpenAPI docs:

As defined by the JSON Schema Validation vocabulary, data types can have an optional modifier property: format. OAS defines additional formats to provide fine detail for primitive data type

The referenced JSON schema includes this:

Structural validation alone may be insufficient to allow an
application to correctly utilize certain values. The "format"
annotation keyword is defined to allow schema authors to convey
semantic information for a fixed subset of values which are
accurately described by authoritative resources, be they RFCs or
other external specifications.

TLDR: It's a way for generic types like string to be given more semantic meaning (by defining a format: uuid as an example).


It would be great if openapi-ts allowed intercepting of common formats (as defined in the JSON spec referenced above), allowing clients to handle them as branded types / custom classes/validators / etc.

Here's one example that would be useful for me:

OpenAPI spec snippet:

 "components": {
    "schemas": {
      "MyObject": {
        "properties": {
          "Id": {
            "type": "string",
            "format": "uuid4",
            // ^ Relevant portion here
            "title": "Id"
          },
// ... definition continues

I'm not sure what a middleware spec inside hey-api would look like, but hopefully this issue gets the ball rolling on client-defined conversion of known subtypes of primitives like string/number.

Thanks for considering!

@ntjess ntjess added the feature 🚀 New feature or request label Mar 25, 2025
@mrlubos
Copy link
Member

mrlubos commented Mar 25, 2025

Hey @ntjess, just to understand, what do you want to do with this information? Are you trying to modify the runtime code? Types? Other?

@ntjess
Copy link
Author

ntjess commented Mar 25, 2025

Correct -- the goal would be to ensure types autogenerated by hey-api can distinguish between basic string arguments vs. "strings that must conform to the UUID spec"

In a perfect world, I can indicate in my OpenAPI spec that an argument's format is uuid as above, and replace occurrences of hey-api's generations as follows:

Current behavior:

// In types.gen.ts
export type Metadata = {
    Id?: string;
    // Other fields...
}

Desired optional behavior:

// Intercept a `format: uuid` string and coerce its type into a branded alias.
// These types would be provided by the user, and IDString would be used by hey-api instead
export type IdTemplate = `${string}-${string}-${string}-${string}-${string}`;
export type IDString<T extends IdTemplate = IdTemplate> = T & {
  readonly __brand: unique symbol;
};

// Resulting in this hey-api generated type:
export type Metadata = {
    Id?: IDString;
    // Other fields...
}

With this change, I get helpful type errors passing non id-like strings to this API function.

The same format-based validation can apply to dates/times, emails, etc.

@mrlubos
Copy link
Member

mrlubos commented Mar 25, 2025

Ooh, I see. No runtime transformation needed? As long as it's scoped to types like this, it shouldn't be too difficult to add. Do you have a preference around how you'd want to define these types? Should it be handled perhaps out of the box?

@ntjess
Copy link
Author

ntjess commented Mar 25, 2025

Indeed, no runtime transform needed. Just a mapping of format-specified OpenAPI types to their desired autogenerated type specs.

I think it would be hard to define out of the box:

  • Some users would want branded types (as in the ID example above), but others wouldn't want it
  • Some users would impose template validation on the type (as in the example above), others wouldn't
  • It's unclear what level of validation users want on types like UUID/email, since ts doesn't provide regex-based parsing. (Even if it did, regex-parsing a valid email would be out of scope... It's hard)

But, if you only considered branded types out-of-box, it might be doable. E.g. plugins: [..., @hey-api/branded-formats] would encounter format: uuid and export type uuid: string & {__brand: unique symbol} as above. This seems to be common ts convention for type aliases.

The downside is, calling these API functions would require:

sdk.myEndpoint("1-2-3-4-5" as uuid)
//                         ^^^^^^^ explicit cast

which is why not every client would want it by default. I prefer it, since explicit casting like this prevents user error when e.g. passing an email string instead of UUID string.


In an ideal world, I could also control certain aspects of the autogeneration (for instance, exporting the name UUID instead of uuid). These could be extra args in the plugin configuration.

@mrlubos
Copy link
Member

mrlubos commented Mar 25, 2025

Ok so you'll be doing casting like shown above? Do you need uuid branded type to be exported? And how would you imagine this configuration working from the user experience perspective?

@ntjess
Copy link
Author

ntjess commented Mar 26, 2025

There's a few ways I can see this feature working:

  1. Less work for you, more work for end-users: No customization options; the plugin would simply create a branded type for each format encountered, exactly matching the casing/etc from the format.
  2. More work for you, but allows much more customization (just brainstorming):
// file openapi-ts.formats.ts
type UUID = string & { readonly __brand: unique symbol};
type Email = `${string}@${string}.${string}`;

export interface FormatHooks {
  uuid: UUID;
  email: Email;
  duration: `${string} seconds`
  // Add more formats here
};
// file openapi-ts.config.ts
plugins: [... "@hey-api/branded-formats"]

This plugin would search for a file named openapi-ts.formats.ts, which defines an interface as above, and replace encountered formats with their specified types. You can either disallow inline types (like duration above), or just use the format name for the type.

I'm not sure if there's an easier way for the user to pass type information to the config, since ts doesn't allow passing an interface directly to a config.

Thoughts?

@mrlubos
Copy link
Member

mrlubos commented Mar 26, 2025

I like the plugin approach, though I'd be looking at keeping the configuration within @hey-api/typescript unless you feel strongly opposed. No concern about what's more work for who, just what feels right!

@ntjess
Copy link
Author

ntjess commented Mar 30, 2025

Minor note: If this feature is natively supported, it would be nice if the generated Zod models also included the string-specific formats: https://zod.dev/?id=strings

So including the zod plugin could generate models with Id: zod.string().uuid() and so on (for formats that zod explicitly handles)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature 🚀 New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants