go 101 lines · 3 tabs

Strict JSON Config Loading in Go with DisallowUnknownFields

Shared by codesnips Aug 2026
3 tabs
package config

import (
	"encoding/json"
	"fmt"
	"time"
)

type Config struct {
	ListenAddr string   `json:"listen_addr"`
	MaxConns   int      `json:"max_conns"`
	ReadTimeout Duration `json:"read_timeout"`
	Upstreams  []string `json:"upstreams"`
	TLSEnabled bool     `json:"tls_enabled"`
}

type Duration time.Duration

func (d *Duration) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return fmt.Errorf("duration must be a string: %w", err)
	}
	parsed, err := time.ParseDuration(s)
	if err != nil {
		return fmt.Errorf("invalid duration %q: %w", s, err)
	}
	*d = Duration(parsed)
	return nil
}

func (c *Config) Validate() error {
	if c.ListenAddr == "" {
		return fmt.Errorf("listen_addr is required")
	}
	if c.MaxConns <= 0 {
		return fmt.Errorf("max_conns must be positive, got %d", c.MaxConns)
	}
	return nil
}
3 files · go Explain with highlit

This snippet shows how a Go service loads a JSON config file while rejecting any key that doesn't map to a known struct field. By default, encoding/json silently ignores unknown keys, which hides typos like "tiemout" and makes misconfiguration hard to notice until runtime. The Config type tab models the config with explicit json struct tags, so the decoder knows exactly which fields are legal and how durations and byte sizes are represented on the wire.

The core of the strict behavior lives in the Loader tab. Load opens the file and wraps the reader in a json.Decoder, then calls dec.DisallowUnknownFields(). That single call flips the decoder into strict mode: any field in the JSON that has no corresponding struct field produces an error such as json: unknown field "tiemout" instead of being dropped. After decoding, the code calls dec.Token() once more and expects io.EOF; this guards against a second JSON document or trailing garbage in the file, which the standard decoder would otherwise accept silently.

Because raw decode errors are terse, Load wraps them with fmt.Errorf and the %w verb so callers can still errors.Is/errors.As down to the original cause while getting a message that names the file. The Duration custom type demonstrates why struct tags alone aren't enough: JSON has no duration type, so UnmarshalJSON parses a string like "30s" via time.ParseDuration, giving human-friendly config values with validation for free.

Validation beyond shape lives in Validate, invoked right after decoding. It enforces semantic rules — a non-empty ListenAddr, a positive MaxConns — that the type system can't express, returning descriptive errors early rather than letting a zero value cause a confusing failure later.

The main tab wires it together: it reads a path flag, calls LoadConfig, and exits non-zero with the wrapped error on failure. The trade-off of strict mode is rigidity — forward-compatible configs that intentionally carry extra keys will break — so this pattern fits internal services where a typo should be a hard failure, not a config-management system that must tolerate unknown future fields.


Related snips

Share this code

Here's the card — post it anywhere.

Strict JSON Config Loading in Go with DisallowUnknownFields — share card
Link copied