XINCGer/Unity3DTraining · error · ArgumentException
String contains low surrogate not followed by high surrogate
Error message
String contains low surrogate not followed by high surrogate
What it means
When JSON-escaping a string, JsonFormatter encounters a high surrogate (first half of a UTF-16 pair) but the next char is missing or is not the matching low surrogate. Since the pair cannot be encoded as a valid Unicode scalar, it throws ArgumentException rather than emit malformed JSON.
Solutions
- Fix the source data so string fields contain well-formed UTF-16 (validate with char.IsSurrogatePair before assigning)
- Validate/repair strings before setting them on the message (e.g. Encoding.UTF8 round-trip with throwOnInvalidBytes)
- Check any truncation logic and cut on char boundaries, never mid pair
- Catch ArgumentException in the serialization boundary and log/replace the offending field
Example fix
// before string bad = raw.Substring(0, 10); // may split a surrogate pair msg.Name = bad; // after string safe = new string(raw.Substring(0, 10).Where((c, i) => !char.IsHighSurrogate(c) || i + 1 < 10 && char.IsLowSurrogate(raw[i + 1])).ToArray()); msg.Name = safe;
Defensive patterns
Strategy: validation
Validate before calling
static bool IsWellFormedUtf16(string s) { for (int i = 0; i < s.Length; i++) { if (char.IsHighSurrogate(s[i])) { if (i + 1 >= s.Length || !char.IsLowSurrogate(s[i + 1])) return false; i++; } else if (char.IsLowSurrogate(s[i])) return false; } return true; } Type guard
static bool IsWellFormedUtf16(string s) { for (int i = 0; i < s.Length; i++) { if (char.IsHighSurrogate(s[i])) { if (i + 1 >= s.Length || !char.IsLowSurrogate(s[i + 1])) return false; i++; } else if (char.IsLowSurrogate(s[i])) return false; } return true; } Try / catch
try { return formatter.Format(message); } catch (ArgumentException ex) when (ex.Message.Contains("low surrogate")) { log.Warn("Message contains malformed surrogate pair"); return null; } Prevention
- Validate strings decoded from external bytes are well-formed UTF-16
- Never truncate strings at arbitrary indexes; respect surrogate boundaries
- Decode network payloads with the correct encoding instead of raw char casts
When it happens
Trigger: Calling JsonFormatter.Format on a message whose string field contains a lone/stray high surrogate — typically from corrupted input, byte-level string surgery, decoding UTF-8 bytes as UTF-16 incorrectly, or truncating a string mid-surrogate-pair.
Common situations: Strings truncated with Substring/StringBuilder at arbitrary byte counts; data decoded with the wrong encoding; binary data stored in string fields; interop with systems producing malformed UTF-16.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- String contains high surrogate not preceded by low surrogate
- Unhandled dictionary key type:
- Offset must be within the buffer
- Length must be non-negative and within the buffer
- Size limit must be positive
AI-assisted analysis of XINCGer/Unity3DTraining@016f98412e (2026-09-12).
Data as JSON: /api/errors/35b49a8c53ac562e.
Report an issue: GitHub.
Appendix: source
Thrown at NetWorkAndResources/Socket_Protobuff/Assets/Plugins/Google.Protobuf/JsonFormatter.cs:702
internal static void WriteString(TextWriter writer, string text)
{
writer.Write('"');
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (c < 0xa0)
{
writer.Write(CommonRepresentations[c]);
continue;
}
if (char.IsHighSurrogate(c))
{
// Encountered first part of a surrogate pair.
// Check that we have the whole pair, and encode both parts as hex.
i++;
if (i == text.Length || !char.IsLowSurrogate(text[i]))
{
throw new ArgumentException("String contains low surrogate not followed by high surrogate");
}
HexEncodeUtf16CodeUnit(writer, c);
HexEncodeUtf16CodeUnit(writer, text[i]);
continue;
}
else if (char.IsLowSurrogate(c))
{
throw new ArgumentException("String contains high surrogate not preceded by low surrogate");
}
switch ((uint)c)
{
// These are not required by json spec
// but used to prevent security bugs in javascript.
case 0xfeff: // Zero width no-break space
case 0xfff9: // Interlinear annotation anchor
case 0xfffa: // Interlinear annotation separator
case 0xfffb: // Interlinear annotation terminator
View on GitHub (pinned to 016f98412e)