Skip to content

Getting started

Terminal window
npx skills add tylergannon/polytype

The skill gives Codex, Claude Code, and other compatible agents the complete setup, registration, validation, and CI workflow.

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:

Terminal window
go get -tool github.com/tylergannon/polytype/polytype@latest
types.go
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)
Terminal window
go generate ./...
go mod tidy

schema.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.

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 Person
if 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.