fix(decode): report the smallest unknown field deterministically

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-17 23:11:38 +02:00
parent 4def1b3e8b
commit 3c8ac859c0
4 changed files with 42 additions and 4 deletions
+3
View File
@@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Strict decoding reports the smallest unknown key. Several unknown keys in
one table made the message depend on Go's random map iteration order, so
the same document reported different keys across runs.
- A `MarshalTOML` result of `nil` with a nil error fails the marshal with
`MarshalTOML returned a nil value`. The field silently vanished before, and
inside a value array the nil result reached reflection as a zero value and
+16 -3
View File
@@ -107,12 +107,25 @@ func (d *decoder) assignTable(tbl map[string]any, dst reflect.Value) error {
func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
schema := cachedStructSchema(dst.Type())
if d.disallowUnknown {
// Map iteration order is random, so pick the unknown key to report
// deterministically: the smallest one.
unknown := ""
for key := range tbl {
if _, ok := schema.byName[strings.ToLower(key)]; ok {
continue
}
if unknown == "" || key < unknown {
unknown = key
}
}
if unknown != "" {
return fmt.Errorf("interpres: unknown field %q for %s", unknown, dst.Type())
}
}
for key, val := range tbl {
field, ok := schema.byName[strings.ToLower(key)]
if !ok {
if d.disallowUnknown {
return fmt.Errorf("interpres: unknown field %q for %s", key, dst.Type())
}
if schema.embedMaps != nil {
// Leftover keys land in an untagged embedded map, the inverse
// of the encoder inlining that map's entries.
+2 -1
View File
@@ -186,7 +186,8 @@ A typo such as `database_urls` then fails with
`interpres: unknown field "database_urls" for main.Config` instead of a silent
default-zero run. Strictness applies to every struct the decode reaches, at any
depth, including struct elements inside slices; map destinations accept every
key by nature.
key by nature. When several keys are unknown, the message names the smallest
one, so it does not depend on map iteration order.
### Cancellation
+21
View File
@@ -5,6 +5,7 @@ package interpres
import (
"math"
"strings"
"testing"
"time"
)
@@ -316,6 +317,26 @@ func TestDisallowUnknownFields(t *testing.T) {
}
}
func TestDisallowUnknownFieldsReportsSmallestKey(t *testing.T) {
// Map iteration order is random, so the reported key must be chosen
// deterministically: the smallest unknown key, whichever order the map
// iterates in.
type C struct {
Known string `toml:"known"`
}
data := []byte("known = \"x\"\nzeta = 1\nalpha = 2\nmu = 3\n")
for range 20 {
var c C
err := NewDecoder().DisallowUnknownFields().Decode(data, &c)
if err == nil {
t.Fatal("expected error for unknown fields")
}
if !strings.Contains(err.Error(), `unknown field "alpha"`) {
t.Fatalf("err = %v, want the smallest unknown key alpha", err)
}
}
}
func TestSkippedFieldTag(t *testing.T) {
type C struct {
Keep string `toml:"keep"`