github creasty/defaults v1.9.0

2 hours ago

The first release since v1.8.0 (August 2024), and almost entirely other people's
work: seven pull requests that had been waiting between eight months and two years,
adapted onto a rebuilt test suite.

Behavior changes come first, because there are four of them and one is loud. The
exported API is unchanged, so dependent code keeps compiling; what moved is what the
library does with tags that never worked.

[BREAKING] An invalid default value is now an error (#59, by @marxoffice)

A tag that failed to parse used to be discarded silently: the field kept its zero
value and Set returned nil. It now returns an error naming the field, the tag and
the cause.

field Port: invalid default "abc": strconv.ParseInt: parsing "abc": invalid syntax

This is the one to read twice. If you use the idiom from the README —

if err := defaults.Set(&cfg); err != nil {
	log.Fatal(err)
}

— then a tag that has been quietly broken for years will now stop your program at
startup. That is the point of the change, not a side effect of it.

How to tell whether you are affected: look for a tag that never actually applied.

v1.8.0 v1.9.0
int default:"abc" 0, no error error
int8 default:"999" (overflows the type) 0, no error error
int32 default:"1h" (only int64 takes durations) 0, no error error
int default:" 1 " (padded number) 0, no error error
a bad tag inside a struct reached through a pointer field swallowed error (#68)

Errors also gained the field name and tag as a prefix, so string matching on errors
needs updating even where an error was already returned
: a malformed JSON container
tag reported unexpected end of JSON input and now reports
field V: invalid default "[1,2,3": unexpected end of JSON input. The cause is
wrapped with %w, so errors.Is and errors.As reach through to it — *strconv.NumError,
*json.SyntaxError and *json.UnmarshalTypeError are all still matchable.

[BREAKING] default:"" now allocates (#63, by @5p2O5pe25ouT)

An empty tag used to be indistinguishable from no tag at all. Set reads tags with
StructTag.Lookup now, so default:"" means give me this type's zero value while an
absent tag still means leave this field alone.

v1.8.0 v1.9.0
*string default:"" nil pointer to ""
[]string default:"" nil empty, non-nil slice
map[string]int default:"" nil empty, non-nil map

Untagged fields are unchanged and still come back nil. The visible consequence is
serialization: null becomes "", [] or {}, so snapshot and golden-file tests
downstream will move.

A padded duration tag now parses (#55, by @boskuv)

time.Duration with default:" 10s " produced 0 and now produces 10s.

The trim belongs to the duration attempt alone, which is why a padded number is now
an error rather than silently zero (above). Named duration types are covered too —
type Timeout time.Duration parses " 10s " — because the trim sits in the int64
branch instead of keying off time.Duration's exact type.

CanUpdate(nil) no longer panics (#64, by @lovewave02)

It used to panic with reflect: call of reflect.Value.Type on zero Value. It returns
true now. Code that panicked was never working, so nothing can regress here.


Minimum Go version

go.mod moves from go 1.14 to go 1.21. CI tests the declared floor plus every
currently supported release: 1.21, 1.26 and 1.27. testify is a test-only dependency —
consumers never build it.

No API change

Set, MustSet, CanUpdate and Setter are unchanged, and nothing was added or
removed. That is deliberate: this release is behavior and tests, not surface.

Documentation

  • The README now says what a zero value can and cannot express, and that a pointer is
    how you keep the distinction — *bool is how you let an explicit false survive
    default:"true". Six separate reports had run into this: #60, #49, #37, #31, #30 and
    #15. (#51, by @fchikwekwe)
  • encoding.TextUnmarshaler is documented as a second way to set defaults, and it takes
    precedence over defaults.Setter. (#56, by @llorllale)

Internals

  • The test suite was rewritten. One 771-line file built around a single ~120-field
    struct became 15 black-box files exercising only the public API, at 100% statement
    coverage with a make cover gate that fails the build below it. Today's behavior is
    pinned test by test, including the parts that look wrong — those carry a QUIRK or
    BUG comment with a link, so changing one shows up as a deliberate test diff.
    (#65, #72)
  • CI moved to GitHub Actions from a CircleCI config that had stopped running.
    (#65)
  • The map element loop was flattened, and shouldInitializeField now reads only the
    field with the tag hoisted to its caller. Neither changes behavior. (#58, by @gitsang)

Known issues, unchanged by this release

  • time.Duration and a plain int64 share one parser, so default:"1" on a Duration
    means 1ns and an int64 accepts "1h". Not cleanly fixable — reflection cannot tell
    a named duration type from a named int64 (#66).
  • A failing UnmarshalText or UnmarshalJSON is discarded, so the error you see can
    name the wrong parser (#79).
  • SetDefaults is called twice on a pointer-to-struct field (#67).
  • A self-referential type whose tag creates an element recurses until the stack
    overflows (#71).

Upgrading

Run your tests. If Set now returns an error, that tag was not working before — the
message names the field and the value. If a nil pointer, slice or map became an empty
one, check whether anything downstream serializes it.

Thanks

To everyone whose pull request sat unreviewed and is in this release anyway:
@lovewave02, @5p2O5pe25ouT, @boskuv, @fchikwekwe, @marxoffice, @gitsang and
@llorllale.

Full Changelog: v1.8.0...v1.9.0

Don't miss a new defaults release

NewReleases is sending notifications on new releases.