T tomlkit·org
Inspect Formatter Validator Lint Stats Keys Query Convert TOMLJSON JSONTOML TOMLYAML YAMLTOML INITOML TOMLINI .envTOML TOML.env TOMLTS TOMLCSV CSVTOML TOMLXML TOML.properties .propertiesTOML Transform Sort keys Flatten Unflatten Redact Minify Generate Go struct Rust struct Python types JSON Schema Compare Diff Merge

TOML to Go struct

updated 20 August 2026

Paste a config, get the Go types that unmarshal it. Each table becomes a named struct, arrays of tables become slices of structs, datetimes become time.Time, and every field carries a toml:"…" tag matching the original key.

gofmt-shaped output

What this tool does

It infers a type for every key in your TOML and prints the Go declarations. Tables become structs named after their key, in PascalCase. Arrays of tables become []Struct. Scalars map to string, bool, int64, and float64; datetimes map to time.Time and the time import is added only when one is actually present.

The intent it closes: "I have a working config file and I need the struct that reads it." Writing those types by hand is mechanical and error-prone — one mistyped tag and a field silently stays at its zero value.

Fields that appear in only some entries of an array of tables are marked optional: their tag gets ,omitempty, and with Optional fields set to pointers their type becomes *T so you can tell "absent" from "zero".

When you'd reach for it

  • Bootstrap a config loader. Paste the config, get the structs, wire up toml.Unmarshal.
  • Keep types in sync after a config change. Regenerate and diff against the committed file to see what the new keys need.
  • Reverse-engineer someone else's config. The generated types are a compact map of the whole file.
  • Get the tags right. Keys with dashes or underscores need explicit tags; this writes them for you.
  • Share one shape across languages. Generate Go here, then Rust or Python from the same file.

How it works

Type inference first, then naming, then printing.

1. Infer a type per key

Each value is classified: string, bool, integer, float, datetime, array, or table. For an array of tables, every entry is examined and the field set is the union of all of them — so a field present in two entries out of three is still generated, marked optional. Integers and floats mixed in one array unify to float64; genuinely mixed types fall back to interface{}.

2. Name the types and fields

A table's type name is its key in PascalCase; an array's element type uses a singularised key, so [[upstreams]] yields an Upstream. If two different tables would claim the same name, the second gets a numeric suffix rather than being silently merged. Field names are PascalCase so they are exported.

3. Print the declarations

The root struct comes first, then nested types in the order they were discovered. Field names and types are column-aligned the way gofmt would, tags are appended, and the time import is emitted only if a datetime was found.

Options reference

Package / Root type

The package clause and the name of the outermost struct. Config is the convention; use Settings or the service name if that reads better in your codebase.

Tags

toml is what you want for a TOML loader. json is useful when the same struct also serialises to JSON. toml + json writes both, which is common for a config that is also exposed over an API. No tags relies on Go's case-insensitive field matching — fine for simple keys, wrong for anything with a dash or underscore.

Optional fields

Plain types keeps the struct simple: a missing key leaves the zero value. Pointers generates *string, *int64, and so on for fields that were absent from some entries, so nil means "not set" and 0 means "set to zero" — which matters for a port, a limit, or a boolean flag.

Datetimes

time.Time is the natural mapping and both BurntSushi/toml and go-toml handle it. Choose string if the value is a date-shaped label you never do arithmetic on, or if you want to avoid the time import.

Example

Input:

[[upstreams]]
name = "auth"
url = "http://auth:9000"

[[upstreams]]
name = "billing"
url = "http://billing:9000"
weight = 3

Output:

package config

type Config struct {
	Upstreams []Upstream `toml:"upstreams"`
}

type Upstream struct {
	Name   string `toml:"name"`
	URL    string `toml:"url"`
	Weight int64  `toml:"weight,omitempty"`
}

weight appears in only one of the two entries, so it is tagged omitempty. Turn on pointer optionals and it becomes *int64, letting you distinguish an upstream with weight 0 from one with no weight set.

Limits and notes

  • Inference sees one sample. A field that happens to be a whole number in your file becomes int64 even if it can be fractional. Widen it by hand where you know better.
  • Empty arrays and empty tables are untyped. With nothing to look at, they come out as []interface{} or an empty struct. Add one representative entry to the input to get a real type.
  • Names are derived, not designed. Singularisation is a simple rule; [[data]] gives Datum-ish awkwardness sometimes. Rename freely — nothing depends on the generated names but the tags.
  • No validation is generated. Tags describe the mapping, not the constraints. For constraints, generate a JSON Schema alongside.

FAQ

Which TOML library does the output work with?

Both of the common ones: github.com/BurntSushi/toml and github.com/pelletier/go-toml/v2 read the toml:"…" tag and handle time.Time. The generated code has no library-specific types in it.

Why is my integer int64 rather than int?

Because TOML integers are specified as 64-bit signed. int64 is exact everywhere, including on 32-bit builds; change it to int by hand if you prefer and the value range is safe.

How are optional fields decided?

Only within an array of tables, where the tool can compare entries: a field missing from at least one entry is optional. A key that is simply absent from your whole sample cannot be detected — it is not in the input at all.

Can I get JSON tags too?

Yes — set Tags to toml + json and every field gets both, which is what you want when the config struct is also marshalled into an API response.

Does my config leave the browser?

No. Parsing and code generation both run locally, so you can paste a production config safely.