From c0bf0dd552152109815fee5764e2684e1a6d71e6 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:36:04 -0700 Subject: [PATCH] fix(json): do not HTML-escape <, > and & in JSON output json.Marshal escapes <, > and & as \u003c, \u003e and \u0026 so that the result is safe to embed directly in HTML. dasel writes to files and stdout, where that escaping only makes the output harder to read: piping a pyproject.toml through 'dasel -i toml -o json' turned "setuptools>=77.0.3" into "setuptools\u003e=77.0.3". The writer's encoder now uses json.NewEncoder with SetEscapeHTML(false), trimming the newline Encode appends. Adds TestJsonDoesNotEscapeHTMLCharacters, which round-trips a string containing <, > and & and fails on the previous behaviour. Closes #552 --- CHANGELOG.md | 1 + parsing/json/json_test.go | 29 +++++++++++++++++++++++++++++ parsing/json/json_writer.go | 12 +++++++++--- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff41bffe..bd240788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The JSON writer no longer escapes `<`, `>` and `&` as `\u003c`, `\u003e` and `\u0026`. That escaping is only needed when embedding JSON in HTML, so e.g. `dasel -i toml -o json` now emits `"setuptools>=77.0.3"` rather than `"setuptools\u003e=77.0.3"` ([#552](https://github.com/TomWright/dasel/issues/552)). - The TOML writer no longer relies on go-toml encoding a bare value as a document root, which go-toml v2.4 rejects. Selecting a scalar or list and writing it as TOML (e.g. `dasel -i json -o toml 'hello'`) works again. - Writing a list of tables as the top level value now emits a valid inline array of tables (e.g. `[{a = 1}, {a = 2}]`) instead of table headers with an empty key. diff --git a/parsing/json/json_test.go b/parsing/json/json_test.go index 20a7dad6..10d741a3 100644 --- a/parsing/json/json_test.go +++ b/parsing/json/json_test.go @@ -96,6 +96,35 @@ func TestJsonCompact(t *testing.T) { } } +func TestJsonDoesNotEscapeHTMLCharacters(t *testing.T) { + doc := []byte(`{ + "string": "setuptools>=77.0.3 & " +} +`) + reader, err := json.JSON.NewReader(parsing.DefaultReaderOptions()) + if err != nil { + t.Fatal(err) + } + writer, err := json.JSON.NewWriter(parsing.DefaultWriterOptions()) + if err != nil { + t.Fatal(err) + } + + value, err := reader.Read(doc) + if err != nil { + t.Fatal(err) + } + + newDoc, err := writer.Write(value) + if err != nil { + t.Fatal(err) + } + + if string(doc) != string(newDoc) { + t.Fatalf("expected %s, got %s...\n%s", string(doc), string(newDoc), cmp.Diff(string(doc), string(newDoc))) + } +} + func TestNDJSON(t *testing.T) { newReader := func(t *testing.T) parsing.Reader { t.Helper() diff --git a/parsing/json/json_writer.go b/parsing/json/json_writer.go index f173ddf1..e3d639be 100644 --- a/parsing/json/json_writer.go +++ b/parsing/json/json_writer.go @@ -34,11 +34,17 @@ func (j *jsonWriter) Write(value *model.Value) ([]byte, error) { } encoderFn := func(v any) error { - res, err := json.Marshal(v) - if err != nil { + // json.Marshal escapes <, > and & as \u003c, \u003e and \u0026. + // That is only required when embedding JSON in HTML, so use an + // encoder with HTML escaping disabled to keep the output readable. + valBuf := new(bytes.Buffer) + enc := json.NewEncoder(valBuf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { return err } - _, err = buf.Write(res) + // Encode appends a trailing newline that we do not want here. + _, err := buf.Write(bytes.TrimSuffix(valBuf.Bytes(), []byte("\n"))) return err }