Skip to content

Vendor Extensions ​

Hono Takibi reads x-* vendor extensions on your OpenAPI / JSON Schema to customize the generated Zod. Each extension maps 1:1 to a Zod feature.

Custom error messages ​

One extension per JSON Schema keyword, named x-<keyword>-message.

yaml
name:
  type: string
  minLength: 1
  maxLength: 50
  x-error-message: 'Name must be a string'
  x-minLength-message: 'Name cannot be empty'
  x-maxLength-message: 'Name must be at most 50 characters'
ts
z.string({ error: 'Name must be a string' })
  .min(1, { error: 'Name cannot be empty' })
  .max(50, { error: 'Name must be at most 50 characters' })

Common (any schema type) ​

ExtensionApplies to
x-error-messageAll schemas (fallback when keyword-specific message is absent)
x-required-messageRequired properties
x-const-messageconst
x-enum-messageenum

Numeric (number / integer) ​

ExtensionApplies to
x-minimum-messageminimum
x-maximum-messagemaximum
x-exclusiveMinimum-messageexclusiveMinimum
x-exclusiveMaximum-messageexclusiveMaximum
x-multipleOf-messagemultipleOf

String ​

ExtensionApplies to
x-minLength-messageminLength
x-maxLength-messagemaxLength
x-pattern-messagepattern
x-length-messageExact length (minLength === maxLength)

Array ​

ExtensionApplies to
x-minItems-messageminItems
x-maxItems-messagemaxItems
x-uniqueItems-messageuniqueItems
x-contains-messagecontains
x-minContains-messageminContains
x-maxContains-messagemaxContains

Object ​

ExtensionApplies to
x-minProperties-messageminProperties
x-maxProperties-messagemaxProperties
x-additionalProperties-messageadditionalProperties
x-propertyNames-messagepropertyNames
x-patternProperties-messagepatternProperties
x-dependentRequired-messagedependentRequired
x-dependentSchemas-messagedependentSchemas

Combinators ​

ExtensionApplies to
x-allOf-messageallOf
x-anyOf-messageanyOf
x-oneOf-messageoneOf
x-not-messagenot
x-implication-messageImplication pattern (A → B) encoded as anyOf:[{not:A},{required:B}]; takes precedence over x-anyOf-message

Conditional ​

ExtensionApplies to
x-if-messageif
x-then-messagethen
x-else-messageelse

Typeless / Array Applicator ​

ExtensionApplies to
x-properties-messageproperties (typeless schemas)
x-prefixItems-messageprefixItems
x-items-messageitems
x-unevaluatedProperties-messageunevaluatedProperties
x-unevaluatedItems-messageunevaluatedItems

Behavior extensions ​

String Pre-validation Transforms ​

ExtensionGeneratedValue
x-trimz.string().trim()true
x-toLowerCasez.string().toLowerCase()true
x-toUpperCasez.string().toUpperCase()true
x-normalizez.string().normalize('NFC')'NFC' / 'NFD' / 'NFKC' / 'NFKD'
yaml
homepage:
  type: string
  format: url
  x-trim: true
ts
z.string().trim().pipe(z.url())

String Validation Checks ​

ExtensionGeneratedValue
x-lowercasez.string().lowercase()true
x-uppercasez.string().uppercase()true
yaml
slug:
  type: string
  x-lowercase: true
ts
z.string().lowercase()

Preprocess ​

x-preprocess

yaml
username:
  type: string
  x-preprocess: 'z.preprocess((val) => typeof val === "string" ? val.trim() : val, z.string())'
ts
z.preprocess((val) => (typeof val === 'string' ? val.trim() : val), z.string())

Type Coercion ​

x-coerce

yaml
asNumber:
  type: number
  x-coerce: true
asDate:
  type: string
  format: date-time
  x-coerce: true
ts
z.coerce.number()
z.coerce.date()

x-stringbool

yaml
notify:
  type: boolean
  x-stringbool: true
ts
z.stringbool()
yaml
notify:
  type: boolean
  x-stringbool:
    truthy: ['yes', 'on']
    falsy: ['no', 'off']
    case: 'sensitive'
ts
z.stringbool({ truthy: ['yes', 'on'], falsy: ['no', 'off'], case: 'sensitive' })

Codec ​

x-codec

yaml
updatedAt:
  type: string
  format: date-time
  x-codec: 'z.codec(z.iso.datetime(), z.date(), { decode: (isoString) => new Date(isoString), encode: (date) => date.toISOString() })'
ts
z.codec(z.iso.datetime(), z.date(), {
  decode: (isoString) => new Date(isoString),
  encode: (date) => date.toISOString(),
})

Custom Validation ​

x-refine

yaml
password:
  type: string
  x-refine: '.refine((val) => val.length >= 8, { message: "Password must be at least 8 characters" }).refine((val) => /[A-Z]/.test(val), { message: "Password must contain an uppercase letter" })'
ts
z.string()
  .refine((val) => val.length >= 8, { message: 'Password must be at least 8 characters' })
  .refine((val) => /[A-Z]/.test(val), { message: 'Password must contain an uppercase letter' })

x-superRefine

yaml
normalizedEmail:
  type: string
  format: email
  x-superRefine: '.superRefine((val, ctx) => { if (val.endsWith("@blocked.example")) ctx.addIssue({ code: "custom", message: "Blocked domain" }) })'
ts
z.email().superRefine((val, ctx) => {
  if (val.endsWith('@blocked.example')) {
    ctx.addIssue({ code: 'custom', message: 'Blocked domain' })
  }
})

Transform & Pipe ​

x-transform

yaml
code:
  type: string
  x-transform: 'z.string().transform((val) => val.toUpperCase())'
ts
z.string().transform((val) => val.toUpperCase())

x-pipe

yaml
port:
  type: string
  x-pipe: 'z.string().pipe(z.number().int().positive())'
ts
z.string().pipe(z.number().int().positive())

Default & Fallback Values ​

x-prefault

yaml
greeting:
  type: string
  x-prefault: 'hello'
ts
z.string().prefault('hello')

x-catch

yaml
retries:
  type: integer
  x-catch: 0
ts
z.int().catch(0)

Immutability ​

x-readonly

yaml
config:
  type: object
  properties:
    name:
      type: string
  x-readonly: true
ts
z.object({ name: z.string() }).readonly()

String Content Checks ​

x-startsWith / x-endsWith / x-includes

yaml
url:
  type: string
  x-startsWith: 'https://'
  x-endsWith: '.com'
path:
  type: string
  x-includes: '/api/'
ts
z.string().startsWith('https://').endsWith('.com')
z.string().includes('/api/')

Format-Specific Options ​

yaml
htmlEmail:
  type: string
  format: email
  x-emailPattern: 'html5' # email pattern preset
uuidV7:
  type: string
  format: uuid
  x-uuidVersion: v7 # uuid version
httpsUrl:
  type: string
  format: uri
  x-urlProtocol: '^https$' # url protocol regex
  x-urlNormalize: true
preciseDatetime:
  type: string
  format: date-time
  x-isoPrecision: 3 # iso datetime precision / offset / local
  x-isoOffset: true
ExtensionMaps toValues
x-emailPatternz.email({ pattern })html5 / rfc5322 / unicode
x-emailRegexz.email({ pattern: /.../ })custom regex string
x-uuidVersionz.uuid({ version })v1 / v2 / v3 / v4 / v5 / v6 / v7 / v8
x-urlProtocolz.url({ protocol: /.../ })regex string
x-urlHostnamez.url({ hostname: /.../ })regex string
x-urlNormalizez.url({ normalize })true / false
x-isoPrecisionz.iso.datetime({ precision })fractional second digits
x-isoOffsetz.iso.datetime({ offset })true (default) / false
x-isoLocalz.iso.datetime({ local })true / false
x-macDelimiterz.mac({ delimiter }): / - / .
x-jwtAlgz.jwt({ alg })HS256 etc.
x-hashAlgz.hash(alg, ...)sha256 etc.
x-hashEncz.hash(alg, { enc })hex / base64 / base64url

date-time and time are RFC 3339 values, so both take an offset by default: 2024-01-01T00:00:00+09:00 and 12:34:56+09:00 are valid beside 2024-01-01T00:00:00Z. Set x-isoOffset: false to take Z only for a date-time (z.iso.datetime()), and no zone at all for a time (z.iso.time()).

Branded types (x-brand) ​

Generates Zod branded types: nominal types that are structurally identical but distinct to the type checker.

yaml
components:
  schemas:
    Cat:
      type: object
      properties:
        name:
          type: string
      required:
        - name
      x-brand: Cat
    Dog:
      type: object
      properties:
        name:
          type: string
      required:
        - name
      x-brand: Dog
ts
// Generated output
const CatSchema = z.object({ name: z.string() }).brand<'Cat'>().openapi('Cat')

const DogSchema = z.object({ name: z.string() }).brand<'Dog'>().openapi('Dog')

Released under the MIT License.