Getting started
Optional: teach your coding agent
Section titled “Optional: teach your coding agent”npx skills add tylergannon/polytypeThe skill gives Codex, Claude Code, and other compatible agents the complete setup, registration, validation, and CI workflow.
1. Add the pinned generator
Section titled “1. Add the pinned generator”polytype requires Go 1.27 or newer.
The recommended installation uses Go’s tool directive. It pins the generator in
go.mod, so developers and CI run the same version:
go get -tool github.com/tylergannon/polytype/polytype@latest2. Define the type and generation command
Section titled “2. Define the type and generation command”package contacts
import "github.com/tylergannon/polytype"
//go:generate go tool polytype --validate
// Person is a contact extracted from a document.type Person struct { // Full legal name. Name string `json:"name"`
// Email address. Omit when the source does not provide one. Email polytype.Optional[string] `json:"email,omitzero"`
// Required key; null means no phone number was supplied. Phone polytype.Nullable[string] `json:"phone"`}Doc comments become schema descriptions, so write them for the model that will produce the JSON.
3. Create the build-tagged registration file
Section titled “3. Create the build-tagged registration file”Write schema.go with one panic stub per generated method and one
Declare line per root type, then generate:
//go:build jsonschema
package contacts
import ( "encoding/json"
"github.com/tylergannon/polytype")
func (Person) Schema() json.RawMessage { panic("not implemented") }func (Person) ValidateJSON(_ []byte) error { panic("not implemented") }
var _ = polytype.Declare(Person.Schema)go generate ./...go mod tidyschema.go is compiled only during generation. The generated
jsonschema_gen.go is compiled during normal builds, so the files never define
the same methods together.
Programmatic generation
Section titled “Programmatic generation”Generators can use the same declaration values without a build-tagged registration file or schema accessor:
config := polytype.Declare[model.Envelope]()err := codegen.Gen(config, codegen.Target("./model"), codegen.TypeScript("./web/generated", true), codegen.Devalue("./transport/codec_gen.go", "transport", "example.com/project/transport"),)Select codegen.JSONSchema() or codegen.GoJSON() when those outputs are
needed. polytype.Compose combines multiple declarations and sealed-union
settings into one request for a generator program. In schema.go, write each
Declare and SealedUnion as its own var _ = declaration instead; there,
polytype.Declare[T]() generates T’s Go JSON codecs, and TypeScript with
--typescript, without a schema file or accessor.
4. Generate TypeScript with the Go JSON boundary
Section titled “4. Generate TypeScript with the Go JSON boundary”For a Go and TypeScript integration, pin an explicit release that contains both capabilities. The tool and imported marker/runtime package come from the same Go module release. Add validation and the TypeScript destination to one directive:
//go:generate go tool polytype --validate --typescript web/src/generated --typescript-barrel--typescript-barrel is optional. There is no separate codec flag: a
registered struct whose fields use sealed-interface unions or .StringerEnum
enums automatically receives generated JSON codecs, and every marked enum
(func (T) enum()) declared in the package receives a type-level
MarshalJSON/UnmarshalJSON pair that rejects non-members. The run writes
types.ts and, when requested, index.ts alongside the schema and Go outputs.
Pin one module version for the generator and imported runtime packages so their APIs stay in sync.
The TypeScript files are structural declarations. They do not contain runtime
decoders or validators. TypeScript code can use JSON.parse and
JSON.stringify, but must validate untrusted runtime data itself.
Each CLI run owns its TypeScript output directory. Give different target Go
packages different output directories because a later run replaces the
generated types.ts. A custom generator can combine roots from several
packages by lowering them together with grammar and calling
typescript.Generate once.
5. Use the generated schema and Go boundary
Section titled “5. Use the generated schema and Go boundary”schema := Person{}.Schema()if err := (Person{}).ValidateJSON(llmOutput); err != nil { return err}var person Personif err := json.Unmarshal(llmOutput, &person); err != nil { return err}Use json.Marshal on the containing Go struct for outgoing JSON. Sealed-union
and string-mode enum fields use the generated owner codec automatically.
Commit schema.go, jsonschema_gen.go, the entire jsonschema/ directory
including .json.sum files, and any requested generated TypeScript files.
The checksum marks its matching schema as generator-owned. Removing or renaming
a registration prunes its unchanged schema pair on the next generation; a
modified orphan is preserved and reported.
Next, choose the field semantics you need in Optional and nullable, jump to Validation and CI, or, for a SvelteKit boundary, generate devalue transport codecs.