Self-hosted service
ogen-go/ogen avatar
ogen-go/ogen

ogen turns an OpenAPI v3 document into typed Go servers and clients

OpenAPI v3 code generator for go

2,146 stars195 forksGoApache-2.0

At a glance

What is it?
ogen is a Go code generator that reads an OpenAPI v3 specification and emits a package of types, a radix router, a server interface and a matching client, with no reflection at run time. The trade is a committed generated tree you must regenerate and review whenever the spec moves.
Who is it for?
ogen fits teams whose OpenAPI v3 document is the contract and whose service is large enough that a generated package pays for itself. It does not fit handlers that have drifted ahead of the schema, or a team unwilling to review regenerated diffs on every spec change.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • ogen-go/ogen2,146 ★ · +0
Star history
Repository2026-10-152026-10-23
ogen-go/ogen2.1k2.1k

0 Stars in the last 8 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The generated Server interface is the contract you implement

The centre of ogen's output is not a client but an interface you write by hand. The README shows the shape of it: a `Server` interface with one method per operation, each taking a context and a params struct and returning the response type plus an error.

go
type Server interface {
	PetGetByName(ctx context.Context, params PetGetByNameParams) (Pet, error)
	// ...
}

The client method for the same operation keeps the name and the params struct but returns `(res Pet, err error)`, so call sites read the same from either side. The asymmetry sits on the server signature: a success body and an error, nothing else, so any status other than the documented success case has to travel back out through the error value. Around that interface the generator builds request parsing, a router and response encoders. The repository layout mirrors those pieces, with `gen/` driving generation, `http/` holding server scaffolding, `internal/` carrying templates and integration fixtures, and `ogen.go` and `spec.go` at the root holding the top-level entry points.

Encoding and validation are compiled in, never resolved at request time

Every decision about a value is made when the code is written. The README states there is no reflection and no `interface{}` in the generated types: JSON encoding is generated and runs through `go-faster/jx` rather than `encoding/json`, and validation is generated from the spec rather than performed by a checker at run time. String formats map onto Go types directly, so `uuid`, `date`, `date-time` and uri become `uuid.UUID`, `time.Time`, `time.Time` and `url.URL` instead of strings parsed later by each call site.

The sample Pet struct in the README is the clearest evidence: `Birthday time.Time`, `IP net.IP`, `Rate time.Duration`, `URI url.URL`, `UniqueID uuid.UUID`. Recursive shapes that Go cannot express, such as a Pet holding a slice of Pet, are the documented exception, where pointers come back as a fallback. What you pay for the mapping is a compile-time dependency on jx and a bigger generated file. What you get back is that a malformed request fails before your handler runs, and a response that violates the schema cannot be serialised at all.

Optional, nullable and the array nil rule the README admits is unfinished

Absence and null are separate concepts in ogen, and the generator refuses to collapse them into pointers. Each property becomes one of three generated wrappers, `Optional[T]`, `Nullable[T]` or `OptionalNullable[T]`, each a small struct with presence and null flags:

go
type OptNilString struct {
	Value string
	Set   bool
	Null  bool
}

Four helpers come with it, `Get`, `IsNull`, `IsSet`, `IsEmpty`, plus a `NewOptNilString` constructor. The array case is where the documentation stops short of the spec. When an array is optional, nil means no value; when it is nullable, nil means the value is null; when it is required, nil currently behaves the same as an empty slice, and the README calls that state actually invalid. For a nullable required array the wrapper is listed as a TODO. A generator admitting an open case in its own feature list is telling you the JSON Schema corner is not closed. Check how your own schema uses required arrays before you rely on that branch.

oneOf becomes a sum type with four discrimination strategies

A `oneOf` union does not collapse into Go any. A union of `[string, integer]` becomes a struct with a type tag and both fields, with constructors `NewStringID` and `NewIntID` beside it. Deciding which variant arrived takes 4 passes, and the order matters. An explicit `discriminator` in the schema settles it outright. Failing that, primitive variants are separated by inspecting the JSON type at run time. Failing that, ogen compares variant fields: different field names separate two variants, the same name with a different type separates them, and identical names and types with different enum values separate them last.

That last strategy is the fragile one, because it works only if schema authors use enum values as a kind of tag. The README does not say what happens when two variants share every field name, every field type and every enum value. There is no basis for the choice at generation time, and the documentation is silent on the fallback.

Generating your first package with go install, go:generate or the container

Installation is one command against the module path, and the generator is meant to run from a go:generate directive rather than by hand so the output is reproducible in CI:

bash
go install -v github.com/ogen-go/ogen/cmd/ogen@latest

Then point it at your document. The README usage line names the flags this project actually has, `--target` for the output directory, `-package` for the package name and `--clean` to remove stale files:

go
//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target target/dir -package api --clean schema.json

What you should see afterwards is a populated directory holding the types, the router, the server interface and the client. Running the directive again after editing the spec rewrites that tree in place.

If Go is not on the machine, the same generator ships as a container image. Note that the volume mounts at `/workspace`, so both the output directory and the spec path live inside the mount:

shell
docker run --rm \
  --volume ".:/workspace" \
  ghcr.io/ogen-go/ogen:latest --target workspace/petstore --clean workspace/petstore.yml

The image carries two binaries, `/ogen` and `/jschemagen`, and the second one is worth knowing about because the repository builds it from the same source tree.

The radix router and params structs replace the mux you would have written

Path matching is generated as a static radix tree, so routes and their parameters are fixed when the code is written rather than registered at boot by a chain of patterns. The same idea explains why arguments, headers and query values arrive as a struct: a path like `/pet/{name}` becomes a `PetGetByNameParams` holding a `Name string`, and the client fills in the same struct on the way out. Adding or renaming an operation means regenerating, not editing a registration list, which is where the payoff sits for a service with more than a handful of routes.

The project keeps a wide example corpus to exercise that path. Directories under `examples/` cover petstore, GitHub, Kubernetes, Telegram, OAuth2 and more, and the Makefile keeps them in their own module through separate examples and test_examples targets. A generator regression then shows up as broken example code instead of a silently wrong type. OpenTelemetry tracing and metrics, on otel v1.45.0 in the module file, plus Server-Sent Events are generated rather than bolted on afterwards.

Where the generated code stops matching the spec

The generated types are not the document, and the gaps sit in documented corners. Recursive schemas that Go cannot express fall back to pointers, which puts back the nil ambiguity the wrappers exist to remove. Required arrays accept nil as an empty slice while the README calls that value invalid, and a nullable required array has no wrapper yet.

The repository also ships an `ex_swagger_petstore` example even though the tool describes itself as an OpenAPI v3 generator, so verify what your input parser accepts before feeding it a Swagger 2.0 document. Toolchain versions matter too. `go.mod` declares `go 1.25.0`, while the Dockerfile builds with `CGO_ENABLED=0` on a `latest` Go base and lands on `gcr.io/distroless/static-debian13` with the Go toolchain copied in so `goimports` can run, a choice its comment ties to dropping a libssl3 package that surfaced in a vulnerability scan as CVE-2026-31789. Upgrading means regenerating and reading the diff, and the Makefile has a `commit_gen` target that commits generated files deliberately. The project is Apache-2.0 and the last push was on 2026-09-28.

Hand-written handlers as the honest alternative

Generated code is a commitment, so the real alternative is keeping the specification as documentation and writing the handlers yourself against `fasthttp` or the standard library server. That path has no codegen step in CI, no generated package to rebuild after a spec change, and no committed diff to review. Every argument is parsed by code you own and can trace with a debugger.

The trade is direct. ogen wins when the schema is the source of truth and the service has enough operations for a router, encoders and validators to matter, because those three are written once by the generator instead of once per handler, and the client and server cannot drift from each other. Hand-written handlers win when the schema lags behind reality, which is common for an API that grew organically: then the generated interface and the running server disagree, and the fix is to correct the document rather than the Go file.

Editorial conclusion

ogen fits teams whose OpenAPI v3 document is the contract and whose service is large enough that a generated package pays for itself. It does not fit handlers that have drifted ahead of the schema, or a team unwilling to review regenerated diffs on every spec change. Before adopting, generate one package from your real document and read two things: the array case, where a required array with a nil value is documented as invalid yet currently behaves like an empty slice, and the TODO on a nullable required array, which has no wrapper yet.

Frequently asked questions

How do I install ogen and generate a Go package from an OpenAPI v3 spec?

Install the binary with `go install -v github.com/ogen-go/ogen/cmd/ogen@latest`, then run it through a go:generate directive using `--target` for the output directory, `-package` for the package name and `--clean` to drop stale files. A container image is also published if Go is not available locally.

Does ogen generate Go code that relies on reflection or interface{}?

The README states there is no reflection or `interface{}`, with JSON encoding generated on top of `go-faster/jx` and validation generated from the spec. It does document one exception: parameters with no type in the schema are represented as Go any and encoded with `fmt.Sprint` on the client.

What happens to optional and nullable array fields in generated code?

For an optional array nil means absence, for a nullable array nil means the value is null, and for a required array nil currently behaves like an empty slice even though the README calls that state invalid. Generating a wrapper for an array that is both nullable and required is still listed as a TODO.

Can ogen read a Swagger 2.0 document?

The README describes the project as an OpenAPI v3 code generator and does not document a Swagger 2.0 input mode. The repository does contain an `ex_swagger_petstore` example directory, so check what the parser accepts before migrating an older document.

Does the generated server include tracing and metrics?

The README lists OpenTelemetry tracing and metrics among the generated features, and `go.mod` requires several `go.opentelemetry.io/otel` modules at v1.45.0. Server-Sent Events support is listed the same way.

Official sources

  1. License: Apache-2.0
  2. ogen-go/ogen on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ogen-go-ogen.svg)](https://hysenlabs.com/projects/ogen-go-ogen)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/ogen-go-ogen.svg)](https://hysenlabs.com/projects/ogen-go-ogen)

Was this useful?