YAML to Go Struct Converter

Paste a YAML config and get Go struct definitions with nested named types, exported field names, and struct tags matched to the decoder you import. Everything runs in this tab, so a file holding database passwords never leaves your machine.

Conversion workbench

Ctrl+Enter converts
Paste YAML on the left to start.
Loader for the selected decoder

What comes out of one config file

The generator reads the whole document, not one line at a time. A mapping becomes a struct, a mapping under a key becomes a second named struct, and a list of mappings becomes a slice of one merged struct. Scalars are typed from their text: unquoted digits become int, decimals become float64, true and false become bool, everything else is string. Field columns are padded the way gofmt pads them, so the output drops into a file without a reformat pass.

Input
server:host: api.toolexe.com
port: 8443
tls: true
retries: [1, 2, 5]
Output
type Config struct {Server Server `yaml:"server"`
Retries []int `yaml:"retries"`}
type Server struct {Host string `yaml:"host"`
Port int `yaml:"port"`
TLS bool `yaml:"tls"`}

Nested types are emitted in document order after the root, parent before child, so a reader scrolling the file meets Config first and each type where its field was declared. Tick inline anonymous structs to fold everything into one declaration instead. Inline reads fine for a two-level file and turns into a wall of braces past three.

Exported names follow the initialism list, not the key

Go exports a field by capitalising its first letter, so every key gets a capital. Words split on the underscore, the hyphen, the dot and every camelCase boundary. Words on the initialism list used by golint and by revive, the linter most golangci-lint setups run, are written in full caps: ID, URL, HTTP, TLS, SQL, JSON, YAML and the rest. A key starting with a digit is not a legal identifier, so the name gets an N prefix. Two keys collapsing to the same name inside one mapping, such as user_id and userId, get a numeric suffix on the second one. The tag keeps the original spelling either way, so decoding still lands on the right field.

user_id→UserIDhttp_port→HTTPPortapiVersion→APIVersionssl_mode→SSLModemax-conns→MaxConns2fa_enabled→N2faEnabled

Struct type names come from the key too, singularised for lists: servers becomes []Server, policies becomes []Policy. When two keys at different depths share a name but hold different fields, the deeper one is prefixed with its parent, giving ServiceNested rather than a compile error. Two keys with identical fields share one type.

The import path decides the tag key

Nothing in a YAML file tells you which struct tag to write. The import line does. Pick the decoder in the rail and the tag checkboxes follow: yaml.v3 and goccy/go-yaml read yaml tags, while sigs.k8s.io/yaml turns the document into JSON first and hands the bytes to encoding/json, so only json tags count there. Ticking both boxes is the right call for a struct shared between a config loader and an HTTP handler, at the cost of longer lines.

PackageTag readBehaviour worth knowing
gopkg.in/yaml.v3yaml:"key"Without a tag, matches the field name lowercased. Ignores json tags. Refuses a file with a duplicate key. KnownFields(true) on a decoder rejects keys with no field.
sigs.k8s.io/yamljson:"key"Used by kubectl and most operators. yaml tags are ignored. UnmarshalStrict rejects unknown and duplicate keys. Bare dates fail on time.Time fields.
github.com/goccy/go-yamlyaml:"key"Same tag key as yaml.v3 with a faster decoder. yaml.Strict() passed to UnmarshalWithOptions rejects unknown and duplicate keys.
github.com/spf13/vipermapstructure:"key"viper.Unmarshal goes through mapstructure. Neither tag above applies. Add mapstructure tags by hand or set a decoder hook.

The loader block under the output writes the matching Load function for whichever decoder is selected, with the strict variant when reject unknown keys is on. Strict mode is worth the friction for config files: a typo like timout surfaces at startup as an error instead of as a default value nobody asked for.

Numbers, quotes and nulls

An unquoted 8080 becomes int. The same value in quotes stays string, because the quotes are the author's way of saying so, and yaml.v3 refuses to put "8080" into an int field at runtime. Tick int64 when the values are IDs or byte counts, or when the team prefers the width spelled out. Any decimal or exponent form becomes float64, and a list mixing 1 and 2.5 widens to float64 as a whole.

A null on its own carries no type, so the field becomes any and a note asks you to fill the type in by hand. A null seen next to a real value inside a list becomes a pointer: *int for one item with port: null and another with port: 1. The pointer is how Go tells "absent" apart from "zero", which matters for a port, a timeout or a replica count.

YAML 1.1 spellings such as yes, no, on and off are strings here. yaml.v3 follows YAML 1.2 for these when decoding into any and reads them as booleans only when the target field is already bool. Keep true and false in your files and the question never comes up.

Dates such as 2026-09-21 become time.Time only when the option is ticked. yaml.v3 decodes a bare timestamp into time.Time without help. sigs.k8s.io/yaml fails on the same value, because encoding/json expects an RFC 3339 string with a time part. String is the default for this reason, and a quoted date always stays a string.

Lists of blocks and the missing-field rule

A list of mappings becomes one struct, named from the singular of the key. The generator reads every item, not the first one. Keys are unioned in order of first appearance, types are merged per key, and a key absent from any item gets omitempty on its tag so decoding does not stall on the items without one. The notes strip under the output lists every field where this happened, so you know which ones to treat as optional in code.

Items with nothing in common, say strings mixed with mappings, fall back to []any. So does an empty list, since nothing in the file says what belongs there.

The uniform maps option treats a mapping whose values all share one scalar type as map[string]T instead of a struct. Labels, annotations and environment lookups read better this way. Leave the option off for records, because a name/value pair matches the rule too and loses its field names.

Kubernetes manifests: import the upstream types instead

A Deployment pasted here produces a struct with the right shape and the wrong guarantees. The generated Replicas is int. Upstream, the same field is *int32. A resource quantity such as 500m becomes string, where the upstream type is resource.Quantity with arithmetic and comparison. Labels become map[string]string, which matches, and most of the rest does not.

For anything the API server already knows, import k8s.io/api/apps/v1 and decode with sigs.k8s.io/yaml. Use this converter for your own config files, and for the spec block of a custom resource you are still drafting, where no upstream type exists yet.

Where this converter stops

  1. Only the first document in a multi-document stream is converted. The notes tell you how many were found. Decode the rest with yaml.NewDecoder and one Decode call per document.
  2. Anchors and merge keys are resolved and flattened. Two aliases of one anchor produce two copies of the fields, not a shared type or a pointer.
  3. Tags such as !!binary, !Ref and !include are dropped and the value is typed from its text. Comments are not carried across.
  4. Complex keys starting with ? and flow collections spanning several lines are not parsed. Keep flow style on one line or switch to block style.
  5. Type names are derived from keys and never checked against your package. A key named error or string yields a type called Error or String, which compiles and reads badly.
  6. Inference sees one file. A field holding port: 8080 here and port: "8080" in a second environment file gets int, and the second file fails to decode. Run the converter on the widest example you have, or on a merge of several.

YAML to Go questions

Why does the output use yaml tags and not json tags?

The tag follows the decoder selected in the rail. gopkg.in/yaml.v3 and goccy/go-yaml read yaml tags. sigs.k8s.io/yaml converts the document to JSON and reads json tags. Tick both boxes to write both.

Why did the id key become ID rather than Id?

ID is on the initialism list golint and revive enforce, so Id triggers a var-naming warning in most golangci-lint configurations. The tag still reads id, so nothing changes at decode time.

Why is one field a pointer?

A list item had null for a value where another item had a real one. A pointer is the only Go type where nil and zero are different things, so *int keeps the distinction a plain int would lose.

Are comments from the YAML kept?

No. The parser drops comments before typing anything. Copy them across as Go comments once the struct is in your editor.

Does the output pass gofmt?

Field names, types and tags are padded into the columns gofmt would produce, and indentation uses tabs. Run gofmt before committing anyway, since your editor will do so on save.

Is the YAML sent to a server?

No. Parsing and generation run in the page script. Nothing is uploaded, logged or stored.