Repository: pelletier/go-toml (2025-08-21T06:47:24Z)

Issue:
UnmarshalTOML isn't called when expected
**Describe the bug**
UnmarshalTOML isn't being called when enabled.

**To Reproduce**
Run the following code.
Observe that it runs without issue even though I have a unavoidable panic in the UnmarshalTOML call.
```
package main

import (
	"bytes"
	"fmt"
	"github.com/pelletier/go-toml/v2"
	"github.com/pelletier/go-toml/v2/unstable"
)

type doc struct {}

func (d *doc) UnmarshalTOML(value *unstable.Node) error {
	panic("we should see this panic")
}

var _ unstable.Unmarshaler = (*doc)(nil)

func main() {
	tomlBytes := []byte(`foo = "bar"`)
	var d doc
	if err := toml.NewDecoder(bytes.NewReader(tomlBytes)).
		EnableUnmarshalerInterface().
		Decode(&d); err != nil {
		panic(err)
	}
	fmt.Println(d) // {}
}
```

**Expected behavior**
UnmarshalTOML should be called as the decode target implements the interface, resulting in the panic.

**Versions**
 - go-toml: release v2.2.4
 - go: go version go1.24.1 darwin/arm64
 - operating system: macOS

**Additional context**
I understand this is in the unstable branch but I would love to get it working.
I'm looking to implement a solution to maintain map ordering.



Discussion:
I believe this should help https://github.com/pelletier/go-toml/pull/996
Feel free to reopen if it's not enough! It doesn't deal with tables defined piece-wise yet though.
I'm trying to implement something like this: https://github.com/TomWright/dasel/blob/adad4edfb98f14481cd06b957c5962fae5ef0e31/parsing/yaml/yaml.go#L88C22-L88C28

When decoding the following bytes:
```go
[]byte(`title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
`)
```

I first get a String, TOML Example. node.Next = Key, title.

I'm a little confused about the expected ordering here - is the root node handled using UnmarshalTOML too?



Patch:
diff --git a/unmarshaler.go b/unmarshaler.go
index 189be525..eda6dd33 100644
--- a/unmarshaler.go
+++ b/unmarshaler.go
@@ -1154,6 +1154,17 @@ func (d *decoder) handleKeyValuePart(key unstable.Iterator, value *unstable.Node
 	case reflect.Struct:
 		path, found := structFieldPath(v, string(key.Node().Data))
 		if !found {
+			// If no matching struct field is found but the target implements the
+			// unstable.Unmarshaler interface (and it is enabled), delegate the
+			// decoding of this value to the custom unmarshaler.
+			if d.unmarshalerInterface {
+				if v.CanAddr() && v.Addr().CanInterface() {
+					if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
+						return reflect.Value{}, outi.UnmarshalTOML(value)
+					}
+				}
+			}
+			// Otherwise, keep previous behavior and skip until the next table.
 			d.skipUntilTable = true
 			break
 		}


Test patch:
diff --git a/unmarshaler_test.go b/unmarshaler_test.go
index 6ee66361..fa386858 100644
--- a/unmarshaler_test.go
+++ b/unmarshaler_test.go
@@ -3998,3 +3998,45 @@ foo = "bar"`,
 		})
 	}
 }
+
+type doc994 struct{}
+
+func (d *doc994) UnmarshalTOML(value *unstable.Node) error {
+	return errors.New("expected-error")
+}
+
+func TestIssue994(t *testing.T) {
+	var _ unstable.Unmarshaler = (*doc994)(nil)
+	tomlBytes := []byte(`foo = "bar"`)
+	var d doc994
+	err := toml.NewDecoder(bytes.NewReader(tomlBytes)).
+		EnableUnmarshalerInterface().
+		Decode(&d)
+
+	assert.Error(t, err)
+
+	if err.Error() != "expected-error" {
+		t.Fatalf("expected error 'expected-error', got '%s'", err.Error())
+	}
+}
+
+type doc994ok struct {
+	S string
+}
+
+func (d *doc994ok) UnmarshalTOML(value *unstable.Node) error {
+	d.S = string(value.Data) + " from unmarshaler"
+	return nil
+}
+
+func TestIssue994_OK(t *testing.T) {
+	var _ unstable.Unmarshaler = (*doc994ok)(nil)
+	tomlBytes := []byte(`foo = "bar"`)
+	var d doc994ok
+	err := toml.NewDecoder(bytes.NewReader(tomlBytes)).
+		EnableUnmarshalerInterface().
+		Decode(&d)
+
+	assert.NoError(t, err)
+	assert.Equal(t, "bar from unmarshaler", d.S)
+}