package main import ( "fmt" "io" "sort" "strings" ) func emitZod(w io.Writer, schemas []Schema, aliases []Alias) error { if _, err := fmt.Fprintln(w, zodHeader); err != nil { return err } for _, a := range sortAliases(aliases) { if _, err := fmt.Fprintf(w, "export const %sSchema = %s;\n", a.Name, zodTypeExpr(a.Underlying)); err != nil { return err } if _, err := fmt.Fprintf(w, "export type %s = z.infer;\n\n", a.Name, a.Name); err != nil { return err } } for _, s := range sortSchemas(schemas) { if _, err := fmt.Fprintf(w, "export const %sSchema = z.object({\n", s.Name); err != nil { return err } fields := append([]Field(nil), s.Fields...) sort.SliceStable(fields, func(i, j int) bool { return fields[i].JSONName < fields[j].JSONName }) for _, f := range fields { line := fmt.Sprintf(" %s: %s,\n", quoteIfNeeded(f.JSONName), zodExpr(f)) if _, err := fmt.Fprint(w, line); err != nil { return err } } if _, err := fmt.Fprintln(w, "});"); err != nil { return err } if _, err := fmt.Fprintf(w, "export type %s = z.infer;\n\n", s.Name, s.Name); err != nil { return err } } return nil } func zodExpr(f Field) string { expr := zodTypeExpr(f.Type) expr = applyZodValidations(expr, f.Type, f.Validate) if f.Optional { expr += ".optional()" } return expr } func zodTypeExpr(t TypeRef) string { switch t.Kind { case KindString: return "z.string()" case KindBool: return "z.boolean()" case KindInt: return "z.number().int()" case KindNumber: return "z.number()" case KindAny, KindUnknown: return "z.unknown()" case KindRaw: return "z.unknown()" case KindArray: return "z.array(" + zodTypeExpr(*t.Element) + ")" case KindMap: return "z.record(" + zodTypeExpr(*t.Key) + ", " + zodTypeExpr(*t.Value) + ")" case KindRef: if t.Name == "nullable" { return zodTypeExpr(*t.Inner) + ".nullable()" } return "z.lazy(() => " + t.Name + "Schema)" } return "z.unknown()" } func applyZodValidations(expr string, t TypeRef, rules []ValidateRule) string { for _, r := range rules { switch r.Name { case "required": continue case "omitempty": continue case "gte": if t.Kind == KindInt || t.Kind == KindNumber { expr += fmt.Sprintf(".min(%s)", r.Param) } case "lte": if t.Kind == KindInt || t.Kind == KindNumber { expr += fmt.Sprintf(".max(%s)", r.Param) } case "gt": if t.Kind == KindInt || t.Kind == KindNumber { expr += fmt.Sprintf(".gt(%s)", r.Param) } case "lt": if t.Kind == KindInt || t.Kind == KindNumber { expr += fmt.Sprintf(".lt(%s)", r.Param) } case "min": if t.Kind == KindString { expr += fmt.Sprintf(".min(%s)", r.Param) } else if t.Kind == KindInt || t.Kind == KindNumber { expr += fmt.Sprintf(".min(%s)", r.Param) } case "max": if t.Kind == KindString { expr += fmt.Sprintf(".max(%s)", r.Param) } else if t.Kind == KindInt || t.Kind == KindNumber { expr += fmt.Sprintf(".max(%s)", r.Param) } case "url": expr += ".url()" case "email": expr += ".email()" case "oneof": values := strings.Fields(r.Param) quoted := make([]string, 0, len(values)) for _, v := range values { quoted = append(quoted, fmt.Sprintf("'%s'", v)) } expr = fmt.Sprintf("z.enum([%s])", strings.Join(quoted, ", ")) } } return expr } func quoteIfNeeded(name string) string { if name == "" { return "''" } for i, r := range name { if r >= 'a' && r <= 'z' { continue } if r >= 'A' && r <= 'Z' { continue } if r == '_' || r == '$' { continue } if i > 0 && r >= '0' && r <= '9' { continue } return "'" + name + "'" } return name } const zodHeader = `// Code generated by tools/openapigen. DO NOT EDIT. import { z } from 'zod';`