XINCGer/Unity3DTraining · error · InvalidProtocolBufferException

Invalid field mask

Error message

Invalid field mask: {text}

What it means

Google.Protobuf's JsonParser throws this InvalidProtocolBufferException when converting a FieldMask from its JSON snake_case representation back to proto field paths. In a field mask, an underscore must always be followed by an ASCII letter that becomes the capitalized character of the camelCase proto field name. An underscore at the end of the string, or followed by anything other than a letter, cannot be mapped to a valid proto path, so the parser rejects it.

Solutions

  1. Fix the JSON input: each '_' in a field mask must be immediately followed by a letter (the snake_case-to-camelCase encoding), e.g. "user.profileName", never "user__id" or "user_."
  2. Remove trailing underscores from field mask strings before sending them to ParseJson.
  3. If generating field masks programmatically, use a canonical encoder (e.g. FieldMask.ToString / the WellKnownTypes FieldMask helpers) instead of hand-building strings.
  4. If you control the producer, validate field masks against the target message's field names before serialization.

Example fix

// before
var mask = json["updateMask"].ToString(); // "user__email"
var parsed = JsonParser.Default.Parse<UpdateRequest>(json); // throws
// after
string mask = json["updateMask"].ToString().Replace("__", "_").TrimEnd('_'); // "user_email"
var parsed = JsonParser.Default.Parse<UpdateRequest>(json);
Defensive patterns

Strategy: validation

Validate before calling

static bool IsValidFieldMaskJson(string mask) =>
    !string.IsNullOrEmpty(mask) && !mask.EndsWith("_") && !mask.Contains("__");
// check each '_' is followed by [A-Za-z]
if (!IsValidFieldMaskJson(mask)) throw new FormatException("Invalid field mask: " + mask);

Type guard

static bool IsValidFieldMask(string mask)
{
    for (int i = 0; i < mask.Length; i++)
        if (mask[i] == '_' && (i + 1 >= mask.Length || !char.IsAsciiLetter(mask[i + 1])))
            return false;
    return true;
}

Try / catch

try
{
    var msg = JsonParser.Default.Parse<TRequest>(json);
}
catch (InvalidProtocolBufferException ex) when (ex.Message.Contains("Invalid field mask"))
{
    // log/repair the field mask string and retry
}

Prevention

When it happens

Trigger: Calling JsonParser.ParseJson (or any descriptor-driven FromJson) on JSON containing a FieldMask value with a trailing underscore (e.g. "user._"), a double underscore ("user__id"), or an underscore followed by a non-letter character. The throw site is in the field-mask path conversion loop at JsonParser.cs:959, reached whenever a field of type google.protobuf.FieldMask is parsed from JSON.

Common situations: Hand-written JSON where a field mask was typed with snake_case separators instead of valid camelCase-with-single-underscore encoding; code that built a field mask string by string concatenation leaving a trailing '_'; interoperating with a service that emits non-canonical field masks; migrating from a client library with laxer field-mask validation.

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/69181d92cfa2a6e2. Report an issue: GitHub.

Appendix: source

Thrown at NetWorkAndResources/Socket_Protobuff/Assets/Plugins/Google.Protobuf/JsonParser.cs:959

                    if (wasNotUnderscore &&               //            case 1 out
                        (wasNotCap ||                     // case 2 in, case 3 out
                         (i + 1 < text.Length &&         //            case 3 out
                          (text[i + 1] >= 'a' && text[i + 1] <= 'z')))) // ascii_islower(text[i + 1])
                    {  // case 4 in
                       // We add an underscore for case 2 and case 4.
                        builder.Append('_');
                    }
                    // ascii_tolower, but we already know that c *is* an upper case ASCII character...
                    builder.Append((char)(c + 'a' - 'A'));
                    wasNotUnderscore = true;
                    wasNotCap = false;
                }
                else
                {
                    builder.Append(c);
                    if (c == '_')
                    {
                        throw new InvalidProtocolBufferException("Invalid field mask: " + text);
                    }
                    wasNotUnderscore = true;
                    wasNotCap = true;
                }
            }
            return builder.ToString();
        }
        #endregion

        /// <summary>
        /// Settings controlling JSON parsing.
        /// </summary>
        public sealed class Settings
        {
            /// <summary>
            /// Default settings, as used by <see cref="JsonParser.Default"/>. This has the same default
            /// recursion limit as <see cref="CodedInputStream"/>, and an empty type registry.
            /// </summary>

View on GitHub (pinned to 016f98412e)