npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zod-to-amplify-dsl

v0.2.0

Published

Convert Zod schemas to AWS Amplify Gen 2 DSL

Readme

zod-to-amplify-dsl

Convert Zod v4 schemas to AWS Amplify Gen 2 TypeScript DSL.

日本語版 README はこちら

Target: Amplify Gen 2 — generates code for @aws-amplify/data-schema v1.x. Amplify Gen 1 (GraphQL SDL / @model directives) is not supported.


Install

npm add zod-to-amplify-dsl
# or
pnpm add zod-to-amplify-dsl

Quick start

npx zod-to-amplify init   # create starter schema.ts + zod-amplify.config.ts
npx zod-to-amplify --dry  # preview generated output
npx zod-to-amplify        # write to amplify/data/resource.ts

CLI

zod-to-amplify [options]
zod-to-amplify watch [options]
zod-to-amplify init [--force]

zod-to-amplify (generate)

| Flag | Alias | Default | Description | |---|---|---|---| | --input <file> | -i | schema.ts | TypeScript file exporting Zod models | | --output <file> | -o | amplify/data/resource.ts | Output file path | | --dry | | false | Print output to stdout without writing | | --check | | false | Verify output matches what's on disk; exit 1 on drift (CI) | | --json | | false | Output JSON schema metadata instead of TypeScript |

--check regenerates in memory and compares against the committed files (including the generated storage file). It writes nothing and exits non-zero when anything is missing or stale — handy as a CI guard against forgetting to re-run the generator after editing the schema.

zod-to-amplify watch

Same flags as generate. Watches the input file and regenerates on every save.

zod-to-amplify init

Creates schema.ts and zod-amplify.config.ts in the current directory.

| Flag | Description | |---|---| | --force | Overwrite existing files |

zod-to-amplify mcp

Starts an MCP server over stdio so an AI agent can run the converter as a tool. Register it with your MCP client via npx:

{
  "mcpServers": {
    "zod-to-amplify": {
      "command": "npx",
      "args": ["-y", "zod-to-amplify-dsl", "mcp"]
    }
  }
}

Exposed tools (read-only — they never write files):

| Tool | Input | Returns | |---|---|---| | usage | (none) | A guide on how to write a schema file and use the tools | | convert_schema | { schemaPath } | Generated Amplify DSL (with storage code + warnings appended as comments) | | schema_summary | { schemaPath } | JSON summary (zodToAmplifyMeta) |

schemaPath is a .ts file exporting Zod models, resolved relative to the server's working directory.


Config file

Create zod-amplify.config.ts in your project root (optional — CLI flags take precedence):

import { defineConfig } from "zod-to-amplify-dsl"

export default defineConfig({
  input: "src/schema.ts",
  output: "amplify/data/resource.ts",
  // storageOutput: "amplify/storage/resource.ts", // default: sibling of `output`
  // storageName: "media",                         // defineStorage({ name })
})

Schema file

Export Zod models from the input file. Use getter syntax to define circular/forward references between models.

// schema.ts
import { z } from "zod"
import { defineModel } from "zod-to-amplify-dsl"

export const Post = defineModel(
  z.object({
    id: z.uuid(),
    title: z.string().max(200),
    status: z.enum(["DRAFT", "PUBLISHED"]),
    authorId: z.string(),
    createdAt: z.iso.datetime(),

    // Relations: use getter to avoid circular reference issues
    get author(): z.ZodObject<any> { return User },
    get comments(): z.ZodArray<z.ZodObject<any>> { return z.array(Comment) },
    get tags(): z.ZodArray<z.ZodObject<any>> { return z.array(Tag) },
  }),
  {
    indexes: [{ name: "byAuthor", pk: "authorId", sk: "createdAt" }],
    auth: [
      { allow: "owner", ownerField: "authorId" },
      { allow: "public", operations: ["read"] },
    ],
  }
)

export const User = z.object({
  id: z.uuid(),
  name: z.string(),
  email: z.email(),
  get posts(): z.ZodArray<z.ZodObject<any>> { return z.array(Post) },
})

export const Comment = z.object({
  id: z.string(),
  body: z.string(),
  postId: z.string(),
  get post(): z.ZodObject<any> { return Post },
})

// Mutual array references → junction model (PostTag) is auto-generated
export const Tag = z.object({
  id: z.string(),
  name: z.string(),
  get posts(): z.ZodArray<z.ZodObject<any>> { return z.array(Post) },
})

z.lazy(() => Model) also works as an alternative to getter syntax.


Programmatic API

zodToAmplify(models)

Returns { code: string, warnings: ConversionWarning[] }.

import { z } from "zod"
import { zodToAmplify } from "zod-to-amplify-dsl"

const Post = z.object({ id: z.string(), title: z.string() })
const { code, warnings } = zodToAmplify({ Post })

if (warnings.length > 0) {
  console.warn("Unsupported types:", warnings)
}
console.log(code)

zodToAmplifyMeta(models)

Returns a JSON-serializable SchemaSummary — useful for tooling or validation.

import { zodToAmplifyMeta } from "zod-to-amplify-dsl"

const meta = zodToAmplifyMeta({ Post, User })
// meta.models[].fields, .relations, .primaryKey, .indexes, .auth
// meta.customTypes[].fields
// meta.warnings
// meta.storage  → [{ path, access }]

Storage (S3) fields

Amplify Gen 2 has no native file/image data type — files live in S3 and the data model only stores the S3 key. Wrap a string field with storageField() to mark it: the field becomes a.string() in data/resource.ts, and a separate amplify/storage/resource.ts is generated with a matching defineStorage.

import { z } from "zod"
import { storageField } from "zod-to-amplify-dsl"

export const Post = z.object({
  id: z.uuid(),
  coverImage: storageField(z.string(), {
    path: "media/posts/*",
    access: [
      { allow: "guest", to: ["read"] },
      { allow: "owner", to: ["read", "write", "delete"] },
    ],
  }).optional(),
})

Generated data/resource.ts (excerpt):

Post: a.model({
  id: a.id(),
  coverImage: a.string(), // zod: storage(path="media/posts/*")
})

Generated amplify/storage/resource.ts:

import { defineStorage } from "@aws-amplify/backend"

export const storage = defineStorage({
  name: "media",
  access: (allow) => ({
    "media/posts/*": [
      allow.guest.to(["read"]),
      allow.entity("identity").to(["read", "write", "delete"]),
    ],
  }),
})

Notes

  • access is optional. When omitted it defaults to a secure allow.authenticated.to(["read", "write", "delete"]) (no guest access).
  • Fields sharing the same path have their access rules merged and de-duplicated.
  • Allow kinds map as: guest → allow.guest, authenticated → allow.authenticated, owner → allow.entity("identity"), groups → allow.groups([...]).
  • Output location: a data/resource.ts output emits storage/resource.ts alongside it; override with storageOutput (and the bucket name with storageName) in the config file. The CLI writes the storage file only when at least one storageField() is present.

Type mapping

Scalars

| Zod | Amplify | Notes | |---|---|---| | z.string() | a.string() | | | z.uuid() | a.id() | legacy z.string().uuid(); also any field named *Id | | z.email() | a.email() | legacy z.string().email() | | z.url() | a.url() | legacy z.string().url() | | z.e164() | a.phone() | E.164 phone number; legacy z.string().e164() | | z.ipv4() / z.ipv6() | a.ipAddress() | legacy z.string().ipv4() / .ipv6() | | z.iso.datetime() | a.datetime() | legacy z.string().datetime() | | z.iso.date() | a.date() | date only; legacy z.string().date() | | z.iso.time() | a.time() | time only; legacy z.string().time() | | z.number() / z.float32() / z.float64() | a.float() | | | z.int() / z.int32() / z.number().int() | a.integer() | | | z.boolean() | a.boolean() | | | z.date() | a.datetime() | | | z.any() / z.unknown() | a.json() | intentional — no warning | | z.record() / z.tuple() | a.json() | intentional — no warning | | z.map() / z.set() / z.bigint() | a.json() | with warning (no faithful representation) | | other | a.json() | with warning |

Enums (hoisted to schema level)

Amplify's a.enum() cannot be used inline in model fields. All enum types are hoisted to the schema level and referenced via a.ref().

| Zod | Generated field | Schema-level entry | |---|---|---| | z.enum(["A", "B"]) | field: a.ref("Field").required() | Field: a.enum(["A", "B"]) | | z.literal("active") | status: a.ref("Status").required() | Status: a.enum(["active"]) | | z.union([z.literal("A"), z.literal("B")]) | kind: a.ref("Kind").required() | Kind: a.enum(["A", "B"]) |

Enums with .default() emit a comment instead of the unsupported chain:

// Zod: status: z.enum(["draft", "published"]).default("draft")
// Generated:
status: a.ref("Status"), // zod: default("draft")

Optional / default

| Zod | Amplify | |---|---| | z.string().optional() | a.string() (no .required()) | | z.string().default("x") | a.string().default("x") | | z.string().nullable() | a.string() (nullable treated as optional) |

Scalar arrays

| Zod | Amplify | |---|---| | z.array(z.string()) | a.string().array().required() | | z.array(z.number().int()) | a.integer().array().required() | | z.array(z.enum([...])) | a.ref("Name").array().required() |

Nested objects (customType)

Non-model z.object() fields are emitted as a.customType():

const Address = z.object({ street: z.string(), city: z.string() })
const User = z.object({ id: z.string(), address: Address })

Generated:

User: a.model({
  id: a.id(),
  address: a.ref("Address").required(),
}),
Address: a.customType({
  street: a.string().required(),
  city: a.string().required(),
}),

Relations

| Pattern | Amplify | |---|---| | get posts() { return z.array(Post) } | a.hasMany("Post", "userId") | | get author() { return User } + FK field userId | a.belongsTo("User", "userId") | | get profile() { return Profile } (no FK on this side) | a.hasOne("Profile", "userId") | | Mutual z.array() on both sides | a.hasMany("AJunctionModel", "fkId") + auto junction model |

manyToMany — Amplify Gen 2 has no a.manyToMany(). When both models have z.array() pointing at each other, a junction model is automatically generated:

// Input: Post.tags ↔ Tag.posts
// Generated:
Post: a.model({ tags: a.hasMany("PostTag", "postId"), ... }),
Tag:  a.model({ posts: a.hasMany("PostTag", "tagId"), ... }),
PostTag: a.model({
  postId: a.id().required(),
  tagId: a.id().required(),
  post: a.belongsTo("Post", "postId"),
  tag: a.belongsTo("Tag", "tagId"),
}),

defineModel options

defineModel(zodSchema, {
  // Composite primary key → .identifier([...])
  primaryKey: ["tenantId", "orderId"],

  // Secondary indexes → .secondaryIndexes(...)
  indexes: [
    { name: "byAuthor", pk: "authorId" },
    { name: "byAuthorDate", pk: "authorId", sk: "createdAt" },
    // queryField → a custom list query: .queryField("listByAuthor")
    { name: "byAuthor2", pk: "authorId", queryField: "listByAuthor" },
  ],

  // Authorization rules → .authorization(...)
  auth: [
    { allow: "owner" },
    { allow: "owner", ownerField: "authorId" },   // custom owner field
    { allow: "public", operations: ["read"] },
    { allow: "groups", groups: ["admin", "editor"], operations: ["create", "update"] },
  ],

  // Per-field authorization → field.authorization(allow => [...])
  fieldAuth: {
    ssn: [{ allow: "owner" }],
  },

  // Disable generated operations → .disableOperations([...])
  disabledOperations: ["delete", "subscriptions"],
})

disabledOperations accepts: queries, mutations, subscriptions, list, get, create, update, delete, onCreate, onUpdate, onDelete.

Auth mapping:

| Rule | Generated | |---|---| | { allow: "owner" } | allow.owner() | | { allow: "owner", ownerField: "f" } | allow.ownerDefinedIn("f") | | { allow: "multipleOwners", ownersField: "f" } | allow.ownersDefinedIn("f") | | { allow: "public" } | allow.publicApiKey() | | { allow: "public", operations: ["read"] } | allow.publicApiKey().to(["read"]) | | { allow: "guest" } | allow.guest() | | { allow: "authenticated" } | allow.authenticated() | | { allow: "group", group: "g" } | allow.group("g") | | { allow: "groups", groups: ["g"] } | allow.groups(["g"]) | | { allow: "custom" } | allow.custom() (Lambda authorizer) |

All rules accept operations (mapped to .to([...])). Owner/group rules accept an optional provider ("userPools" | "oidc"); authenticated also accepts "identityPool". Example: { allow: "authenticated", provider: "oidc", operations: ["read"] } → allow.authenticated("oidc").to(["read"]).


Field validation

Zod constraints on string / integer / float fields are emitted as Amplify field-level .validate() chains:

// z.string().min(1).max(200)  →
title: a.string().validate((v) => v.minLength(1).maxLength(200)).required(),

// z.string().regex(/^[a-z-]+$/)  →
slug: a.string().validate((v) => v.matches("^[a-z-]+$")).required(),

// z.number().min(0).max(100)  → inclusive bounds use gte/lte
score: a.float().validate((v) => v.gte(0).lte(100)).required(),

// z.number().gt(0).lt(1)  → exclusive bounds use gt/lt
ratio: a.float().validate((v) => v.gt(0).lt(1)).required(),

Mapping: min/max (string) → minLength/maxLength, regex → matches, startsWith/endsWith → same; min/max (number) → gte/lte, gt/lt → gt/lt.

Amplify only allows .validate() on a.string(), a.integer(), and a.float(). For other types (e.g. a.email() with a length constraint) the constraints are preserved as an inline comment instead:

email: a.email().required(), // zod: maxLength(50)

Auto-managed fields

createdAt and updatedAt are managed by Amplify. They are emitted without .required() regardless of the Zod schema:

createdAt: a.datetime(),
updatedAt: a.datetime(),

License

MIT