Repository: goccy/go-yaml
Pull request #698 (2025-04-17 07:36:25)

Treat output from TextMarshaler as string
Closes https://github.com/goccy/go-yaml/issues/689.

The original issue was that since `null.StringFrom` implements the `TextMarshaler` interface, `goccy/go-yaml` tries to interpret its output as a YAML document using `e.encodeDocument`. However, because the output contains a special character (in this case, `#`), the parser fails to handle it correctly and returns `nil`.

IIUC, the output from a `TextMarshaler` implementation should not be interpreted as a YAML document. It should be treated as a plain string, since `TextMarshaler` is intended to return a textual representation of a value. This behavior is consistent with `gopkg.in/yaml.v3`.

One side effect I noticed after making this change is related to how `*time.Time` is encoded. Previously, since `*time.Time` also implements `TextMarshaler`, it was being encoded by the code block below. After treating the output from `MarshalText()` as a string, the library started wrapping `*time.Time` values in double quotes. To prevent this, I updated the `encodeByMarshaler` function to handle `*time.Time` explicitly.

```
if marshaler, ok := iface.(encoding.TextMarshaler); ok {
		doc, err := marshaler.MarshalText()
		if err != nil {
			return nil, err
		}
		node, err := e.encodeDocument(doc)
...
}
```



- [x] Describe the purpose for which you created this PR.  
- [x] Create test code that corresponds to the modification

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/goccy/go-yaml/pull/698?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 77.92%. Comparing base [(`d1e3460`)](https://app.codecov.io/gh/goccy/go-yaml/commit/d1e3460292544f8aa9df85f227df26d3d2298bd9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`a75057e`)](https://app.codecov.io/gh/goccy/go-yaml/commit/a75057e85e704e343edf6ebcfee9f8cf87d3b1bb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima).

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #698      +/-   ##
==========================================
+ Coverage   77.89%   77.92%   +0.03%     
==========================================
  Files          22       22              
  Lines        7965     7965              
==========================================
+ Hits         6204     6207       +3     
+ Misses       1348     1346       -2     
+ Partials      413      412       -1     
```

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
goccy: Thank you for your great PR ! LGTM 👍 

Patch:
diff --git a/encode.go b/encode.go
index 813d7fc..dd1b755 100644
--- a/encode.go
+++ b/encode.go
@@ -330,7 +330,7 @@ func (e *Encoder) canEncodeByMarshaler(v reflect.Value) bool {
 		return true
 	case InterfaceMarshaler:
 		return true
-	case time.Time:
+	case time.Time, *time.Time:
 		return true
 	case time.Duration:
 		return true
@@ -400,20 +400,21 @@ func (e *Encoder) encodeByMarshaler(ctx context.Context, v reflect.Value, column
 	if t, ok := iface.(time.Time); ok {
 		return e.encodeTime(t, column), nil
 	}
+	// Handle *time.Time explicitly since it implements TextMarshaler and shouldn't be treated as plain text
+	if t, ok := iface.(*time.Time); ok && t != nil {
+		return e.encodeTime(*t, column), nil
+	}
 
 	if t, ok := iface.(time.Duration); ok {
 		return e.encodeDuration(t, column), nil
 	}
 
 	if marshaler, ok := iface.(encoding.TextMarshaler); ok {
-		doc, err := marshaler.MarshalText()
-		if err != nil {
-			return nil, err
-		}
-		node, err := e.encodeDocument(doc)
+		text, err := marshaler.MarshalText()
 		if err != nil {
 			return nil, err
 		}
+		node := e.encodeString(string(text), column)
 		return node, nil
 	}
 


Test patch:
diff --git a/encode_test.go b/encode_test.go
index 9864725..613931c 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -22,6 +22,16 @@ import (
 var zero = 0
 var emptyStr = ""
 
+type TestTextMarshaler string
+
+func (t TestTextMarshaler) MarshalText() ([]byte, error) {
+	return []byte(t), nil
+}
+
+type TestTextUnmarshalerContainer struct {
+	V TestTextMarshaler
+}
+
 func TestEncoder(t *testing.T) {
 	tests := []struct {
 		source  string
@@ -731,6 +741,31 @@ func TestEncoder(t *testing.T) {
 			map[string]time.Duration{"v": 30 * time.Second},
 			nil,
 		},
+		{
+			"v: 30s\n",
+			map[string]*time.Duration{"v": ptr(30 * time.Second)},
+			nil,
+		},
+		{
+			"v: null\n",
+			map[string]*time.Duration{"v": nil},
+			nil,
+		},
+		{
+			"v: test\n",
+			TestTextUnmarshalerContainer{V: "test"},
+			nil,
+		},
+		{
+			"v: \"1\"\n",
+			TestTextUnmarshalerContainer{V: "1"},
+			nil,
+		},
+		{
+			"v: \"#\"\n",
+			TestTextUnmarshalerContainer{V: "#"},
+			nil,
+		},
 		// Quote style
 		{
 			`v: '''a''b'` + "\n",
@@ -1655,7 +1690,7 @@ func ExampleMarshal() {
 	// a: Hello speed demon
 	// b: 100
 	//
-	// field: 13
+	// field: "13"
 }
 
 func TestIssue356(t *testing.T) {
@@ -1939,3 +1974,7 @@ a: &anc !mytag
 		})
 	}
 }
+
+func ptr[T any](v T) *T {
+	return &v
+}