XINCGer/Unity3DTraining · error · InvalidProtocolBufferException

Expected string value for FieldMask

Error message

Expected string value for FieldMask

What it means

MergeFieldMask requires a google.protobuf.FieldMask to appear in JSON as a comma-separated string of field paths (e.g. "user.displayName,photo"). If the JSON token is not a string value (object, array, number, null), the parser throws 'Expected string value for FieldMask'.

Solutions

  1. Send the FieldMask as a single comma-separated string: "user.displayName,photo".
  2. Convert arrays of paths to string.Join(",", paths) before serializing.
  3. Catch InvalidProtocolBufferException and map array-form masks to the string form before parsing.
  4. Follow the canonical protobuf JSON mapping documented for google.protobuf.FieldMask.

Example fix

// before
string json = "{\"mask\": [\"user\", \"photo\"]}";
// after
string json = "{\"mask\": \"user,photo\"}";
Defensive patterns

Strategy: type-guard

Validate before calling

// Convert array-form field masks to canonical string form before parsing
string ToFieldMaskJson(IEnumerable<string> paths) =>
    "\"" + string.Join(",", paths) + "\"";

Type guard

bool IsFieldMaskString(object v) => v is string s && s.Split(',').All(p => p.Length > 0);

Try / catch

try { var msg = JsonParser.Default.Parse<T>(json); }
catch (InvalidProtocolBufferException ex) when (ex.Message == "Expected string value for FieldMask")
{ log.Warn("FieldMask must be a comma-separated string"); throw new ArgumentException("fieldMask must be a string like \"user,photo\"", ex); }

Prevention

When it happens

Trigger: Parsing JSON where a FieldMask field is given as an array (["user"]) or an object ({"paths": [...]}) instead of the canonical string form "user,photo" via JsonParser.Parse<T>.

Common situations: Clients sending FieldMask as JSON arrays (a common intuition); JSON from non-protobuf serializers; API consumers unfamiliar with the protobuf JSON mapping for well-known types.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of XINCGer/Unity3DTraining@016f98412e (2026-09-12). Data as JSON: /api/errors/9b00888f62b98945. Report an issue: GitHub.

Appendix: source

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

                }
                if (!Duration.IsNormalized(seconds, nanos))
                {
                    throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
                }
                message.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.SetValue(message, seconds);
                message.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.SetValue(message, nanos);
            }
            catch (FormatException)
            {
                throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
            }
        }

        private static void MergeFieldMask(IMessage message, JsonToken token)
        {
            if (token.Type != JsonToken.TokenType.StringValue)
            {
                throw new InvalidProtocolBufferException("Expected string value for FieldMask");
            }
            // TODO: Do we *want* to remove empty entries? Probably okay to treat "" as "no paths", but "foo,,bar"?
            string[] jsonPaths = token.StringValue.Split(FieldMaskPathSeparators, StringSplitOptions.RemoveEmptyEntries);
            IList messagePaths = (IList)message.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(message);
            foreach (var path in jsonPaths)
            {
                messagePaths.Add(ToSnakeCase(path));
            }
        }

        // Ported from src/google/protobuf/util/internal/utility.cc
        private static string ToSnakeCase(string text)
        {
            var builder = new StringBuilder(text.Length * 2);
            // Note: this is probably unnecessary now, but currently retained to be as close as possible to the
            // C++, whilst still throwing an exception on underscores.
            bool wasNotUnderscore = false;  // Initialize to false for case 1 (below)
            bool wasNotCap = false;

View on GitHub (pinned to 016f98412e)