Stop Writing Go Structs by Hand: How gojson Generates Type-Safe Code from JSON
Hook
Every Go developer has spent hours transcribing API responses into struct definitions, only to discover they mistyped a field name after the fifth compile error. What if you could skip that entirely?
Context
Go's static typing is both its greatest strength and its most tedious requirement. When working with external JSON APIs—whether you're integrating with Stripe, GitHub, or your company's microservices—you need struct definitions that exactly match the JSON structure. Miss a field, get the type wrong, or forget a json tag, and you're debugging marshal errors at runtime.
The traditional workflow is painful: copy the JSON response, manually inspect each field, determine the appropriate Go type, write the struct definition, add json tags, handle nested objects by creating additional structs, and finally test everything. For a modest API response with 20 fields and a few nested objects, this easily burns 15-30 minutes. Multiply that across dozens of endpoints, and you're looking at days of pure grunt work. gojson eliminates this entire category of toil by parsing JSON examples and generating correct, compilable Go code automatically.
Technical Insight
At its core, gojson is a type inference engine wrapped in a code generator. When you pipe JSON into the tool, it performs a depth-first traversal of the document structure, analyzing each value to determine its Go type representation. The intelligence lies in how it handles ambiguity and edge cases.
The basic usage is straightforward. Given a JSON response from the GitHub API, you can generate structs in seconds:
curl -s https://api.github.com/repos/ChimeraCoder/gojson | gojson -name=Repository
This produces a fully-formed struct with appropriate types and json tags:
type Repository struct {
ID int64 `json:"id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Private bool `json:"private"`
Owner Owner `json:"owner"`
Description string `json:"description"`
Fork bool `json:"fork"`
StargazersCount int64 `json:"stargazers_count"`
// ... additional fields
}
type Owner struct {
Login string `json:"login"`
ID int64 `json:"id"`
AvatarURL string `json:"avatar_url"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
}
The type inference algorithm makes pragmatic decisions. Numbers without decimals become int64 rather than float64, which covers most use cases but can cause issues with APIs that sometimes return decimal values. Boolean values are correctly identified, and strings remain strings. When gojson encounters a nested object, it automatically generates an inline struct definition, naming it based on the field name or allowing you to customize the naming convention.
Where things get interesting is with arrays and optional fields. Given an array of uniform objects, gojson correctly infers a slice type. However, an empty array presents a challenge—without example data, the tool falls back to []interface{}, which is technically correct but not type-safe. Similarly, if a field is absent from your example JSON, it won't appear in the generated struct.
The tool integrates elegantly into go:generate workflows, allowing you to commit example JSON files alongside your code and regenerate structs when schemas evolve:
//go:generate sh -c "cat api_response.json | gojson -name=APIResponse > models.go"
This makes gojson particularly valuable in microservice architectures where internal API contracts change frequently. Instead of manually updating structs across multiple repositories, teams can maintain canonical JSON examples and regenerate code automatically during builds.
One architectural decision worth noting: gojson generates public (exported) fields by default, capitalizing field names according to Go conventions. The original JSON keys are preserved in struct tags, so marshaling and unmarshaling work transparently. This means user_id in JSON becomes UserID in Go, with the tag json:"user_id" maintaining the mapping. This follows Go idioms perfectly, but it does mean the generated code assumes you want exported fields—there's no option to generate private fields with accessor methods.
Gotcha
The fundamental limitation of example-based generation is that your struct is only as complete as your example. If an API endpoint returns different fields based on context—common with partial responses, permission-based filtering, or optional inclusions—your single example won't capture the full schema. You'll generate a struct that works for your test case but fails in production when additional fields appear or expected fields are absent.
Type inference, while impressive, hits accuracy limits with certain data types. JSON numbers are all treated as floats in the specification, but gojson infers int64 when values lack decimals. This works until an API occasionally returns 42.0 instead of 42, causing unmarshal errors. Time is another pain point: RFC3339 timestamps arrive as strings, and gojson has no way to infer that "2024-01-15T10:30:00Z" should be time.Time rather than string. You'll need to manually edit these fields after generation. Similarly, enums appear as plain strings, UUIDs look like strings, and numeric IDs might be strings in the JSON but integers in your domain model. The tool can't understand semantic meaning, only syntactic structure.
Arrays with mixed types—increasingly common in modern APIs—become []interface{}, forcing you to use type assertions throughout your code. This is particularly frustrating with APIs that return heterogeneous result sets or discriminated unions, both of which are valid JSON but don't map cleanly to Go's type system without custom unmarshaling logic.
Verdict
Use if: You're rapidly prototyping integrations with third-party APIs, dealing with complex nested JSON responses that would take significant time to transcribe manually, working in a microservices environment where internal schemas change frequently and you can maintain canonical example files, or bootstrapping struct definitions that you plan to refine afterward. It's exceptionally valuable for one-off conversions and initial scaffolding work where speed matters more than perfect type precision. Skip if: You're building production systems that require precise numeric types (int vs int64 vs float64), working with APIs that have formal OpenAPI/Swagger specifications (use schema-based generators instead), dealing with APIs where fields are highly variable or optional, or need semantic type inference for timestamps, UUIDs, and enums. Also skip if you only have partial API examples—incomplete structs are worse than no structs because they give false confidence. For production use cases with well-documented APIs, reach for oapi-codegen or quicktype instead.