Skip to content

Rewrite JSON formatter to avoid decoder internals#203

Merged
sibprogrammer merged 1 commit into
sibprogrammer:masterfrom
mikelolasagasti:fix-1.27
Jul 17, 2026
Merged

Rewrite JSON formatter to avoid decoder internals#203
sibprogrammer merged 1 commit into
sibprogrammer:masterfrom
mikelolasagasti:fix-1.27

Conversation

@mikelolasagasti

@mikelolasagasti mikelolasagasti commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Go 1.27 enables the new encoding/json implementation, where json.Decoder no longer exposes the same private tokenState field layout. FormatJson was using reflection to read that unexported field, which caused a panic with Go 1.27rc2 when FieldByName returned a zero reflect.Value.

Track object and array formatting state using the public Decoder Token and More APIs instead. This keeps the existing output behavior while avoiding dependency on encoding/json implementation details, making the formatter work with both Go 1.26 and Go 1.27.

Fixes #202

Summary by CodeRabbit

  • Bug Fixes

    • Improved JSON pretty-printing for nested objects and arrays with clearer, indented output.
    • Enhanced formatting of keys and primitive values (strings, numbers, booleans, and null), including improved delimiter/key highlighting.
    • Added stricter handling for malformed or unexpected JSON, returning explicit errors for unsupported or mismatched structures.
  • Tests

    • Expanded coverage for JSON formatting across multiple input shapes.
    • Added assertions to verify correct error propagation when writing formatted output fails (including immediate and partial write failure scenarios).

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e08f5e1-66c5-4aa4-bc9e-a7793eb2f18e

📥 Commits

Reviewing files that changed from the base of the PR and between 348da75 and 9540fe2.

📒 Files selected for processing (2)
  • internal/utils/utils.go
  • internal/utils/utils_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/utils/utils_test.go
  • internal/utils/utils.go

📝 Walkthrough

Walkthrough

FormatJson replaces reflection-based encoding/json state inspection with recursive token formatting, explicit delimiter validation, and tests for formatted output and writer failures.

Changes

JSON formatting

Layer / File(s) Summary
Recursive token formatting and validation
internal/utils/utils.go, internal/utils/utils_test.go
FormatJson recursively processes JSON delimiters and primitive values, emits formatted colored output, validates token structures, removes reflection-based tokenizer handling, and tests output plus wrapped writer errors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: rewriting the JSON formatter to avoid decoder internals.
Linked Issues check ✅ Passed The rewrite removes reflection on private Decoder state and matches the Go 1.27rc2 panic fix requested by #202.
Out of Scope Changes check ✅ Passed The test updates and writer failure cases support the formatter rewrite and do not appear unrelated to the issue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/utils/utils.go`:
- Line 436: Update FormatJson and all listed fmt.Fprint write sites to propagate
io.Writer errors instead of discarding them. Route the JSON output writes,
including the final newline write near the end of FormatJson, through a checked
helper that returns failures immediately so processContent receives the error
and truncated output is not reported as successful.
- Around line 436-474: Update the container formatting logic around formatToken
and the decoder loops so opening newlines are emitted only when the first child
is encountered, and pre-close newlines are emitted only when index > 0. Apply
the same behavior to both object and array handling, preserving indentation for
non-empty containers. Add empty object and array fixtures to the exact-output
tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 542301a7-ff4b-41b1-a78c-fb66d21f198b

📥 Commits

Reviewing files that changed from the base of the PR and between 326821d and a01c128.

📒 Files selected for processing (1)
  • internal/utils/utils.go

Comment thread internal/utils/utils.go Outdated
Comment thread internal/utils/utils.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/utils/utils_test.go (1)

137-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use t.Run and assert.NoError for table-driven tests.

Wrapping the table-driven test cases in t.Run isolates failures and identifies exactly which input failed. Additionally, using assert.NoError instead of assert.Nil is idiomatic in testify, as it prints the actual error message rather than a generic type mismatch if a failure occurs.

♻️ Proposed refactor
 	for _, testCase := range tests {
-		output := new(strings.Builder)
-		formatErr := FormatJson(strings.NewReader(testCase.input), output, "  ", ColorsDisabled)
-		assert.Nil(t, formatErr)
-		assert.Equal(t, testCase.expected, output.String())
+		t.Run(testCase.input, func(t *testing.T) {
+			output := new(strings.Builder)
+			formatErr := FormatJson(strings.NewReader(testCase.input), output, "  ", ColorsDisabled)
+			assert.NoError(t, formatErr)
+			assert.Equal(t, testCase.expected, output.String())
+		})
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/utils/utils_test.go` around lines 137 - 142, Update the table-driven
loop in the FormatJson test to wrap each case with t.Run using its test-case
name, and replace assert.Nil on formatErr with assert.NoError. Preserve the
existing input, expected output, and ColorsDisabled assertions inside each
subtest.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/utils/utils_test.go`:
- Around line 137-142: Update the table-driven loop in the FormatJson test to
wrap each case with t.Run using its test-case name, and replace assert.Nil on
formatErr with assert.NoError. Preserve the existing input, expected output, and
ColorsDisabled assertions inside each subtest.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b2cb20c0-5e74-466a-8c8e-48e7bf13a991

📥 Commits

Reviewing files that changed from the base of the PR and between a01c128 and 348da75.

📒 Files selected for processing (2)
  • internal/utils/utils.go
  • internal/utils/utils_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/utils/utils.go

Go 1.27 enables the new encoding/json implementation, where json.Decoder
no longer exposes the same private tokenState field layout. FormatJson
was using reflection to read that unexported field, which caused a panic
with Go 1.27rc2 when FieldByName returned a zero reflect.Value.

Track object and array formatting state using the public Decoder Token
and More APIs instead. This keeps the existing output behavior while
avoiding dependency on encoding/json implementation details, making the
formatter work with both Go 1.26 and Go 1.27.

Signed-off-by: Mikel Olasagasti Uranga <mikel@olasagasti.info>
@sibprogrammer

Copy link
Copy Markdown
Owner

Looks good! @mikelolasagasti thank you for your work!

@sibprogrammer
sibprogrammer merged commit 8814218 into sibprogrammer:master Jul 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

errors with Go 1.27rc2

2 participants