From 509b538cd3db8bb4eadc607797fa01dc086767b9 Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Sun, 21 Dec 2025 21:46:53 -0500 Subject: [PATCH] fix(output): omit nulls from YAML output --- internal/pkg/formatter/yaml.go | 15 +++- internal/pkg/formatter/yaml_test.go | 117 ++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/internal/pkg/formatter/yaml.go b/internal/pkg/formatter/yaml.go index 772cbcb..9823fa1 100644 --- a/internal/pkg/formatter/yaml.go +++ b/internal/pkg/formatter/yaml.go @@ -1,6 +1,7 @@ package formatter import ( + "encoding/json" "fmt" "regexp" "strings" @@ -56,7 +57,19 @@ func newYamlSerializer() *yamlSerializer { } func (s *yamlSerializer) serialize(v any, colored bool) (string, error) { - b, err := yaml.Marshal(v) + // there isn't really a good way to drop null values from YAML, + // so we pass it through the JSON marshaller first + jsonBytes, err := json.Marshal(v) + if err != nil { + return "", err + } + + var cleaned any + if err := json.Unmarshal(jsonBytes, &cleaned); err != nil { + return "", err + } + + b, err := yaml.Marshal(cleaned) if err != nil { return "", err } diff --git a/internal/pkg/formatter/yaml_test.go b/internal/pkg/formatter/yaml_test.go index e063d2c..89f13fb 100644 --- a/internal/pkg/formatter/yaml_test.go +++ b/internal/pkg/formatter/yaml_test.go @@ -8,6 +8,22 @@ import ( "github.com/stretchr/testify/require" ) +// dummyStruct is a test struct with omitempty tags to verify null values are dropped +type dummyStruct struct { + Name string `json:"name"` + Age int `json:"age"` + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + Active bool `json:"active"` + Score *int `json:"score,omitempty"` + Nested *nestedStruct `json:"nested,omitempty"` +} + +type nestedStruct struct { + Value string `json:"value"` + Optional *string `json:"optional,omitempty"` +} + func TestPrintYAML_TableDriven(t *testing.T) { tests := []struct { name string @@ -97,3 +113,104 @@ negative: -10 }) } } + +func TestYamlSerializer_serialize_DropsNullValuesWithOmitempty(t *testing.T) { + tests := []struct { + name string + input any + colored bool + expected string + }{ + { + name: "drops null pointer fields with omitempty", + input: dummyStruct{ + Name: "Alice", + Age: 30, + Email: nil, // Should be dropped + Phone: nil, // Should be dropped + Active: true, + Score: nil, // Should be dropped + }, + colored: false, + expected: `active: true +age: 30 +name: Alice +`, + }, + { + name: "keeps non-null pointer fields with omitempty", + input: dummyStruct{ + Name: "Bob", + Age: 25, + Email: stringPtr("bob@example.com"), + Phone: nil, // Should be dropped + Active: false, + Score: intPtr(100), + }, + colored: false, + expected: `active: false +age: 25 +email: bob@example.com +name: Bob +score: 100 +`, + }, + { + name: "drops nested struct with omitempty when nil", + input: dummyStruct{ + Name: "Charlie", + Age: 35, + Email: stringPtr("charlie@example.com"), + Active: false, + Nested: nil, // Should be dropped + }, + colored: false, + expected: `active: false +age: 35 +email: charlie@example.com +name: Charlie +`, + }, + { + name: "keeps nested struct with omitempty when not nil", + input: dummyStruct{ + Name: "David", + Age: 40, + Email: stringPtr("david@example.com"), + Active: false, + Nested: &nestedStruct{ + Value: "test", + Optional: nil, // Should be dropped from nested struct + }, + }, + colored: false, + expected: `active: false +age: 40 +email: david@example.com +name: David +nested: + value: test +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + serializer := newYamlSerializer() + result, err := serializer.serialize(tt.input, tt.colored) + require.NoError(t, err) + + assert.Equal(t, tt.expected, result) + // Verify that "null" doesn't appear in the output + assert.NotContains(t, result, "null") + }) + } +} + +func stringPtr(s string) *string { + return &s +} + +func intPtr(i int) *int { + return &i +}