`JSON(..., encoding=true)` corrupts supplementary Unicode characters
Summary
When the encoding argument of BeegoOutput.JSON is set to true, supplementary Unicode characters with code points above U+FFFF are encoded incorrectly.
For example, the emoji (U+1F600) is emitted as:
{"emoji":"\u1f600"}However, JSON \\\u escape sequences contain exactly four hexadecimal digits. As a result, a JSON decoder interprets \u1f600 as \u1f60 followed by the literal character 0, producing corrupted data.
The decoded value becomes:
"ὠ0"instead of:
""Because the generated JSON can still be parsed successfully, this may result in silent Unicode data corruption rather than an obvious decoding error.
This appears to be related to the older #2638 fix, which padded Unicode code points shorter than four hexadecimal digits, but did not handle supplementary characters above U+FFFF, which require UTF-16 surrogate pairs.
Affected Versions
- Beego
v2.3.10 masterat commit939cfde380bb9f15844ad633b84f037f7da21584
Steps to Reproduce
package main
import (
"encoding/json"
"fmt"
"net/http/httptest"
beecontext "github.com/beego/beego/v2/server/web/context"
)
func main() {
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
ctx := beecontext.NewContext()
ctx.Reset(&beecontext.Response{ResponseWriter: rec}, req)
if err := ctx.Output.JSON(
map[string]string{"emoji": ""},
false,
true,
); err != nil {
panic(err)
}
var decoded map[string]string
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
panic(err)
}
fmt.Printf("wire: %s\n", rec.Body.String())
fmt.Printf("decoded: %q\n", decoded["emoji"])
}Actual Behavior
The generated JSON is:
wire: {"emoji":"\u1f600"}After decoding:
decoded: "ὠ0"The original Unicode character is corrupted.
Expected Behavior
The decoded value should remain:
""If non-ASCII characters are escaped, supplementary characters should be represented using a valid UTF-16 surrogate pair.
For example:
{"emoji":"\ud83d\ude00"}Root Cause
The issue appears to originate from the Unicode escaping logic in stringsToJSON.
The implementation writes each Unicode code point after a single \\\u prefix:
jsons.WriteString("\\\\u")
jsons.WriteString(strconv.FormatInt(int64(rint), 16))This approach works for characters in the Basic Multilingual Plane when they can be represented using a single \\\uXXXX escape.
For code points above U+FFFF, JSON requires a UTF-16 surrogate pair.
For example:
U+1F600should be represented as:
\uD83D\uDE00rather than:
\u1f600Possible Fix
One possible fix would be to detect code points above U+FFFF, use utf16.EncodeRune, and emit both surrogate halves as four-digit \\\uXXXX escapes.
Alternatively, the custom Unicode escaping logic could be replaced with a standard-library-based implementation that guarantees valid JSON escaping.
Regression tests could also be added for cases such as:
U+FFFFU+10000- emoji such as
- mixed BMP and non-BMP Unicode strings
- JSON encode/decode round-trip preservation
I would be happy to submit a PR if this approach is considered appropriate.
Environment
- Beego:
v2.3.10 - Master commit:
939cfde380bb9f15844ad633b84f037f7da21584 - Go:
1.26.3 - OS: Windows
- Architecture:
amd64
The issue appears to be platform-independent.
Source: beego/beego