godotyaml

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 9 Imported by: 0

README

godotyaml

schema_version = 0

go.yaml is the centralized file for the metadata and configuration of Go projects, filling the gaps of go.mod

This repo provides:

  • The spec and standard of go.yaml file. This is the main focus of this README file.
  • A small utility library for parsing go.yaml files to be used by other applications. Usage of this library is completely optional.

Philosophy

  • go.yaml is not a replacement for go.mod and is only used for metadata/configuration that is not natively supported by official go toolchain.
  • There is no overlap between go.yaml and official go tooling (including go.mod) and no such overlap is planned for the future. It means that metadata such as Go version, dependency list, and module name will remain in go.mod
  • go.yaml will/can contain both project-level metadata and settings and external tool/library/package configurations.
  • No tool/library/package will be treated as first-class citizen with special privileges with the exception of official Go toolchain.
  • go.yaml can be validated but an invalid go.yaml will not prevent your Go code from compiling and running.

Anatomy of go.yaml

Sections

A go.yaml file has broadly two sections:

  1. Standardized structures (the fields and values the schema defines)
  2. Arbitrary configuration for external tools/packages/libraries which can be placed inside go.yaml instead of a standalone file. These arbitrary configurations can only be placed under external key
Root reservation

Please note: All keys at the root level are designed by the schema as an standard and arbitrary data should not be placed at the root level. An external tool/package/library can place their config, without limitation, in external object.

Version

go.yaml provides a version for the project. This version is useful for human-readability and accessing it in your application by embedding go.yaml. This version, however, is not a replacement for VCS tag versions and VCS tag versions remain authoritative.

Usage of external

The external object is only used for configs of the tools and each tool should only define their own configuration and should avoid defining category level configs.

For example, a linter (named myLinter for example), should not use external.linter for their config but they should use external.myLinter. Another linter can define another config under external.secondLinter.

If a category of external tools (such as linters, builders, releasers, etc.) use overlapping configurations, validated by community feedback, we will then promote those overlapping those configs to the schema's root. So, using generic configs such as external.linter, external.build, etc. is highly discouraged.

Spec of go.yaml

go.yaml can be validated but all fields are inherently optional.

name: myproject # The name of the project (not the module name)
description: A useful tool # Optional human-readable description of your project
version: 1.26.3 # The version of your project
schema_version: 0 # The version of schema
repository: https://... # The URL of the repository of the project
issue_tracker: https://... # The URL of the issue tracker of the project
homepage: https://... # Optional URL of the project's homepage/website
documentation: https://... # Optional URL of the project's documentation
license: MIT # The license of the project

# The list of authors
# Each author has a required field of `name` and all other fields are optional. You can add no authors or add multiple authors
authors: 
  - name: Jane Doe
    email: jane@example.com
    organization: Vieolo
    url: example.com
  - name: John Smith
  
  
# The map of executable entry points of the project. A project can have multiple entry points to produce executables. The name of the executable (e.g., server, admin, etc.) is the arbitrary nickname you have for the entry point, the `entrypoint` is required and other fields are optional. A library with no executable, naturally, can skip this field entirely
executables:
  server:
    entrypoint: ./cmd/server
    description: HTTP API server.
  admin:
    entrypoint: ./cmd/admin
    description: Administrative CLI.
  other-exec:
    entrypoint: ./other/main


# The `external` is used for third-party external tools/packages/libraries to define their configs. Each tool has to use a key and place their config under that key (e.g., external.myLinter) and should not place their config on the root of the file or directly inside the `external` object to create an isolation for all tools of the project. The `external` objects have no schema or specs and each tool is free to define their own config structure.
external:

  builderTool:
    path: ./main.go
    mode: strict

  myLinter:
    rules:
      - rule1
      - rule2
    ignore-dep: true

  awesomeReleaser:
    target: public

Go library of this repo

This repo, besides the spec of go.yaml, provides a light parser of go.yaml that other applications can use to avoid recreating a parser on their own everytime, even though you are free to use your own implementation.

The godotyaml library focuses on parsing go.yaml based on the schema version and maintains the data integrity and order of the file upon change.

A few points about this library:

  • It is a library and not a CLI or executable
  • It does not validate go.yaml. it only requires the file to be structurally valid YAML in order to parse. Malformed-but-parseable values are surfaced per-accessor (e.g. a non-integer schema_version returns an error from SchemaVersion()), while Load/Parse never reject a structurally valid file.
  • It does not validate the semantic correctness of any value, such as URLs, licenses, etc.
  • It does not enforce any schema on the sub-keys of external and it remains an open space for external tools to define their config
  • It does not refuse to parse a go.yaml file if an unsupported field exists in the root of the file

Installation of library

go get github.com/vieolo/godotyaml

Usage of library

Read root metadata
doc, err := godotyaml.Load("go.yaml")
if err != nil {
    log.Fatal(err)
}

fmt.Println(doc.Name(), doc.Version())

execs, _ := doc.Executables()
fmt.Println(execs["server"].Entrypoint)
Read a tool's config
// Decode external.my-linter into your own type.
type lintConfig struct {
    Linters map[string][]string `yaml:"linters"`
}

var cfg lintConfig
ok, err := doc.DecodeExternalConfig("my-linter", &cfg)
if err != nil {
    log.Fatal(err)
}
if !ok {
    // section not present
}

// Or get the raw node and decode it yourself.
node, ok := doc.GetRawExternalConfig("my-linter")
Write a tool's config
// Updates only external.my-linter; every other section, all comments, and
// key ordering are left untouched.
err := doc.SetExternalConfig("my-linter", map[string]any{
    "linters": map[string]any{
        "enable": []string{"govet", "staticcheck"},
    },
})
if err != nil {
    log.Fatal(err)
}

if err := doc.Save("go.yaml"); err != nil {
    log.Fatal(err)
}

Documentation

Overview

Package godotyaml is the reference Go library for reading and writing go.yaml, a centralized config and metadata file for Go projects.

It is the parser/helper layer other tools depend on so they do not each roll their own YAML handling for go.yaml. A go.yaml file has two zones: a closed set of root project metadata (name, version, schema_version, executables, ...) exposed here as typed accessors, and an open external namespace where each tool stores arbitrary config under external.<toolname>. New builds a document from that root metadata, so a tool offering an `init` command does not have to assemble the file itself. External sections are treated as opaque: the library reads them, hands them back to the caller to decode, and writes a single tool's section back without disturbing the rest of the file.

Writing a section comes in two flavours, and which one a tool wants is a real decision rather than a detail. SetExternalConfig replaces external.<toolname> outright, so keys and hand-written comments inside it that the caller did not supply are discarded. MergeExternalConfig merges into it instead, leaving everything the caller did not mention (including comments) in place, at the cost of never removing anything. RemoveExternalConfig and RemoveExternalConfigKey are how things are removed deliberately.

The yaml.v3 node tree is kept as the internal source of truth so that comments, key ordering, and unknown root keys survive a load/save cycle, and so that updating one tool's section never re-serializes (and never corrupts) the rest of the document.

Non-goals

godotyaml is deliberately small and unopinionated. It does NOT:

  • validate the semantic correctness of any value (URLs, SPDX license identifiers, version strings, and so on are returned verbatim);
  • enforce any schema on the external namespace;
  • refuse to parse files with unknown root keys or unknown schema_version values — both are preserved and surfaced rather than rejected;
  • expose helpers specific to any individual tool. No consuming tool is privileged; every tool's config lives at external.<toolname> on equal footing.

It is not a CLI, not a validator, and not a schema enforcer.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Author

type Author struct {
	Name         string `yaml:"name"`
	Email        string `yaml:"email,omitempty"`
	Organization string `yaml:"organization,omitempty"`
	URL          string `yaml:"url,omitempty"`
}

Author describes a single entry in the root authors field. Only Name is expected to be present and the remaining fields are optional and tools may carry additional information the spec adds later by decoding the raw node directly.

type Document

type Document struct {
	Doc  *yaml.Node // the document node returned by the decoder
	Root *yaml.Node // the root mapping node (doc.Content[0])
}

Document is a parsed go.yaml file.

The underlying yaml.v3 node tree is the source of truth: typed accessors read from it on demand, and mutations edit it in place. Keeping the node tree canonical (rather than unmarshaling into a struct) is what lets the library preserve comments, key ordering, and unknown keys across a load/save cycle.

func Load

func Load(path string) (*Document, error)

reads and parses the go.yaml file at given path

When no file exists at path the returned error satisfies os.IsNotExist (and errors.Is(err, fs.ErrNotExist)), so a caller can tell "this project has no go.yaml" apart from "this go.yaml is broken" without stat-ing the path first.

func New added in v0.2.0

func New(m Metadata, omitDefaults bool) *Document

builds a new go.yaml document from root metadata, for tools that offer an `init` command.

Keys are written in the order the spec lists them. omitDefaults decides what becomes of a field the caller left at its zero value:

  • false produces a scaffold meant to be edited. Every scalar key is written, with a placeholder value for the ones the caller did not set, and authors, executables and external follow as commented-out examples. The point of the examples is that the shape of those three is the part nobody remembers, so having it in the file removes a trip to the spec.
  • true leaves unset keys out of the file altogether, so a caller that knows only the project's name gets a three-line file rather than a scaffold. No examples are written either.

schema_version is written either way. Zero is the current schema version as well as the zero value, and a file that states which schema it was written against is far easier to migrate later.

The examples are commented out rather than written as empty collections so that they are inert: the generated file parses to exactly the metadata the caller passed, and a tool reading it back sees no authors and no executables rather than examples it might mistake for real entries.

Nothing here is validated, in keeping with the rest of the library: an empty name, or a version that is not a version, is written out as given.

No external section is created; call SetExternalConfig or MergeExternalConfig on the result to add one, then Save (or SaveNew) to write the file. A section added that way is written above the commented examples, which stay in the file as documentation for the next tool.

func Parse

func Parse(r io.Reader) (*Document, error)

reads a go.yaml document from the given io.Reader

An empty input yields an empty document with a writable root mapping rather than an error, so callers can build a file from scratch.

func (*Document) Authors

func (d *Document) Authors() ([]Author, error)

Authors returns the root authors field as a list.

The field may appear in the file as a bare string, a single mapping, or a sequence of strings and/or mappings; all forms are normalized to []Author. A bare string becomes an Author with only Name set. It returns nil when the field is absent and an error only if a mapping entry cannot be decoded.

func (*Document) DecodeExternalConfig

func (d *Document) DecodeExternalConfig(name string, out any) (bool, error)

decodes external.<name> into out, reporting whether the section exists. It is a thin convenience over GetRawExternalConfig followed by node.Decode.

func (*Document) Description

func (d *Document) Description() string

Description returns the root description field, or "" if absent.

func (*Document) Documentation

func (d *Document) Documentation() string

Documentation returns the root documentation field (the project's docs URL), or "" if absent.

func (*Document) Executables

func (d *Document) Executables() (Executables, error)

Executables returns the root executables field, or nil if absent. Absence and an empty map are equivalent. It returns an error only if the section does not match the Executables shape.

func (*Document) ExternalConfigNames

func (d *Document) ExternalConfigNames() []string

returns the config names present under external, in document order.

func (*Document) GetRawExternalConfig

func (d *Document) GetRawExternalConfig(name string) (*yaml.Node, bool)

returns the raw yaml.Node for external.<name>, reporting whether the section exists.

The node is returned so the caller can decode it into its own types (node.Decode(&out)) without godotyaml imposing a structure on it. The node is the live tree node; treat it as read-only and use SetExternalConfig to write changes.

func (*Document) Homepage

func (d *Document) Homepage() string

Homepage returns the root homepage field (the project's website), or "" if absent.

func (*Document) IssueTracker

func (d *Document) IssueTracker() string

IssueTracker returns the root issue_tracker field, or "" if absent.

func (*Document) License

func (d *Document) License() string

License returns the root license field, or "" if absent.

func (*Document) MergeExternalConfig added in v0.2.0

func (d *Document) MergeExternalConfig(name string, value any) error

merges value into external.<name>, creating the section if needed, keeping the keys and comments the caller did not supply.

Mappings are merged recursively at every depth: a key present in value takes the value given, and a key absent from value is left exactly as it was, together with any comment attached to it. Sequences and scalars are not merged but replaced wholesale, which is what lets a caller shorten or clear a list by supplying the new one.

A merge never removes anything. A tool that drops a key from its own config struct and then merges will still find the old key in the file, because "absent from value" means "leave it alone", not "delete it". To remove something use RemoveExternalConfigKey for a single key, SetExternalConfigKey to replace one top-level key of the section wholesale, or SetExternalConfig to replace the whole section.

Comments already in the file survive the merge. A comment carried on value (for callers that build their own *yaml.Node) replaces the comment on the matching key, but an empty comment never erases one that is already in the file.

Only external.<name> is touched: sibling sections and the remainder of the file remain intact and untouched.

func (*Document) Name

func (d *Document) Name() string

Name returns the root name field.

func (*Document) RemoveExternalConfig added in v0.2.0

func (d *Document) RemoveExternalConfig(name string) bool

removes external.<name>, reporting whether the section was present.

The external mapping itself is never removed, even when the section removed was the last one. An empty external renders as "external: {}", which keeps any comment written above external; removing the mapping as well would silently discard that comment, which is the kind of loss this library exists to avoid. A later write repopulates the empty mapping in block style as usual.

func (*Document) RemoveExternalConfigKey added in v0.2.0

func (d *Document) RemoveExternalConfigKey(name, key string) bool

removes external.<name>.<key>, reporting whether the key was present.

The comments attached to that key go with it. Neighbouring keys, the rest of the section, and the rest of the file are untouched. Removing the last key of a section leaves an empty section behind; use RemoveExternalConfig to remove the section itself.

func (*Document) Repository

func (d *Document) Repository() string

Repository returns the root repository field, or "" if absent.

func (*Document) Save

func (d *Document) Save(path string) error

writes the document back to the file at path.

The write is atomic. The document is rendered fully in memory, written to a temporary file beside path, flushed, and then renamed over path, so a serialization error, a crash, or a full disk cannot leave a truncated go.yaml behind: path always holds either the previous content or the complete new content. A go.yaml usually carries several tools' configuration as well as the project metadata, so a half-written one is expensive to lose.

An existing file keeps its permission bits exactly; because a rename takes the mode of the temporary file, Save copies the mode across itself. A file that Save creates is created 0644 as modified by the process umask. When path is a symlink the link is resolved and its target replaced, leaving the link itself in place.

func (*Document) SaveNew added in v0.2.0

func (d *Document) SaveNew(path string) error

writes the document to path only if no file is there yet.

This is the write an `init` command wants. A go.yaml holds every tool's configuration as well as the project metadata, so overwriting one that already exists destroys other tools' data; SaveNew refuses instead of clobbering. If path exists the returned error satisfies os.IsExist (and errors.Is(err, fs.ErrExist)), which the caller can report as "this project already has a go.yaml".

The check is not a stat followed by a write: the file is created exclusively, so two `init` runs racing each other cannot both decide the path was free. The content is then written with the same atomic replace Save uses.

func (*Document) SchemaVersion

func (d *Document) SchemaVersion() (int, error)

SchemaVersion returns the root schema_version as an integer.

The schema version is a single incrementing integer (0, 1, 2, ...). There are no minor versions such as 1.1. It returns (0, nil) when the key is absent. If the value is present but not a valid integer it returns a non-nil error: the file still parses (Load/Parse never reject it) and the malformed value is surfaced here rather than silently coerced. Quoted scalars (e.g. "1") are accepted.

func (*Document) SetExternalConfig

func (d *Document) SetExternalConfig(name string, value any) error

writes external.<name>, creating the external section if needed.

The section is REPLACED, not merged: every key currently under external.<name> is discarded, including keys the caller did not supply and any comment written by hand inside the section. A comment attached to the external.<name> key itself survives, because that key node is reused. Use MergeExternalConfig when the section's other keys and comments have to be kept.

value may be any value yaml.v3 can marshal, or an existing *yaml.Node for callers that manage their own subtree (e.g. to preserve their own comments).

This function only touches the target section of `external` and sibling objects and the remainder of the file remains intact and untouched

func (*Document) SetExternalConfigKey added in v0.2.0

func (d *Document) SetExternalConfigKey(name, key string, value any) error

writes external.<name>.<key>, creating the section (and external) if needed.

The value at key is replaced wholesale, so any mapping previously stored there is discarded along with the comments attached to that value. A comment attached to the key itself is kept, as is every other key of the section. Use this to replace one top-level key of a section outright where MergeExternalConfig would merge into it.

If external.<name> exists but does not hold a mapping, it is replaced by one.

func (*Document) Version

func (d *Document) Version() string

Version returns the root version field.

func (*Document) Write

func (d *Document) Write(w io.Writer) error

serializes the document to the given io.Writer, preserving comments and key ordering

Indentation is normalized to two spaces; yaml.v3 does not record the source file's original indent width, so that one stylistic detail is not preserved. Structure and ordering are otherwise reproduced as they were read: key order, comments, unknown root keys, and every external section the caller did not touch are written back unchanged.

Blank lines are NOT preserved. yaml.v3 has no representation for a blank line, so the empty lines a human used to separate keys and sections are all dropped the first time a tool writes the file. This is usually the most visible part of the diff a tool produces, and it is a limitation of the underlying YAML library rather than a choice godotyaml makes.

type Executable

type Executable struct {
	Entrypoint  string `yaml:"entrypoint"`            // directory holding package main, relative to project root
	Description string `yaml:"description,omitempty"` // optional human-readable purpose
}

Executable describes one executable entry point the project produces. It is project metadata only as it carries no output paths, OS/arch targets, build flags, or per-executable versions (every executable inherits the single project version). Build-specific concerns belong in a build tool's external.<toolname> section, not here.

type Executables

type Executables map[string]Executable

Executables maps an author-chosen executable name to its definition. Absence of the root key and an empty map are equivalent (both have length 0): the project produces no executables, i.e. it is a library.

type Metadata added in v0.2.0

type Metadata struct {
	Name          string
	Description   string
	Version       string
	SchemaVersion int
	Repository    string
	IssueTracker  string
	Homepage      string
	Documentation string
	License       string
	Authors       []Author
	Executables   Executables
}

Metadata is the root project metadata of a go.yaml, as a plain struct, so a tool can hand the whole of it to New at once.

Every field is optional, matching the spec. A field left at its zero value is either written with a default value or left out of the file entirely, depending on New's omitDefaults argument.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL