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

  1. Fix the source data so string fields contain well-formed UTF-16 (validate with char.IsSurrogatePair before assigning)
  2. Validate/repair strings before setting them on the message (e.g. Encoding.UTF8 round-trip with throwOnInvalidBytes)
  3. Check any truncation logic and cut on char boundaries, never mid pair
  4. 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

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


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)