Skip to content

Commit 55e50ba

Browse files
C#: Ignore invalid enum string values in JSON, when invalid fields are being ignored.
PiperOrigin-RevId: 605310357
1 parent 959e5df commit 55e50ba

File tree

3 files changed

+115
-33
lines changed

3 files changed

+115
-33
lines changed

conformance/failure_list_csharp.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,3 @@ Required.Proto3.JsonInput.OneofFieldNullSecond.JsonOutput
1313
Required.Proto3.JsonInput.OneofFieldNullSecond.ProtobufOutput
1414
Recommended.Proto3.ValueRejectInfNumberValue.JsonOutput
1515
Recommended.Proto3.ValueRejectNanNumberValue.JsonOutput
16-
Recommended.Proto2.JsonInput.IgnoreUnknownEnumStringValueInMapValue.ProtobufOutput
17-
Recommended.Proto2.JsonInput.IgnoreUnknownEnumStringValueInOptionalField.ProtobufOutput
18-
Recommended.Proto2.JsonInput.IgnoreUnknownEnumStringValueInRepeatedField.ProtobufOutput
19-
Recommended.Proto3.JsonInput.IgnoreUnknownEnumStringValueInMapValue.ProtobufOutput
20-
Recommended.Proto3.JsonInput.IgnoreUnknownEnumStringValueInOptionalField.ProtobufOutput
21-
Recommended.Proto3.JsonInput.IgnoreUnknownEnumStringValueInRepeatedField.ProtobufOutput

csharp/src/Google.Protobuf.Test/JsonParserTest.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using NUnit.Framework;
1414
using ProtobufTestMessages.Proto2;
1515
using ProtobufTestMessages.Proto3;
16+
using ProtobufUnittest;
1617
using System;
1718
using UnitTest.Issues.TestProtos;
1819

@@ -924,6 +925,52 @@ public void Enum_Invalid(string value)
924925
Assert.Throws<InvalidProtocolBufferException>(() => TestAllTypes.Parser.ParseJson(json));
925926
}
926927

928+
[Test]
929+
public void Enum_InvalidString_IgnoreUnknownFields()
930+
{
931+
// When ignoring unknown fields, invalid enum value strings are ignored too.
932+
// This test uses TestProto3Optional so we can check we're not just setting the field to the 0 value.
933+
var parser = new JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true));
934+
string json = "{ \"optionalNestedEnum\": \"NOT_A_VALID_VALUE\" }";
935+
var parsed = parser.Parse<TestProto3Optional>(json);
936+
Assert.IsFalse(parsed.HasOptionalNestedEnum);
937+
}
938+
939+
[Test]
940+
public void RepeatedEnum_InvalidString_IgnoreUnknownFields()
941+
{
942+
// When ignoring unknown fields, invalid enum value strings are ignored too.
943+
// For a repeated field, the value is removed entirely.
944+
var parser = new JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true));
945+
string json = "{ \"repeatedForeignEnum\": [ \"FOREIGN_FOO\", \"NOT_A_VALID_VALUE\", \"FOREIGN_BAR\" ] }";
946+
var parsed = parser.Parse<TestAllTypes>(json);
947+
var expected = new[] { TestProtos.ForeignEnum.ForeignFoo, TestProtos.ForeignEnum.ForeignBar };
948+
Assert.AreEqual(expected, parsed.RepeatedForeignEnum);
949+
}
950+
951+
[Test]
952+
public void EnumValuedMap_InvalidString_IgnoreUnknownFields()
953+
{
954+
// When ignoring unknown fields, invalid enum value strings are ignored too.
955+
// For a map field, the entry is removed entirely.
956+
var parser = new JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true));
957+
string json = "{ \"mapInt32Enum\": { \"1\": \"MAP_ENUM_BAR\", \"2\": \"NOT_A_VALID_VALUE\" } }";
958+
var parsed = parser.Parse<TestMap>(json);
959+
Assert.AreEqual(1, parsed.MapInt32Enum.Count);
960+
Assert.AreEqual(MapEnum.Bar, parsed.MapInt32Enum[1]);
961+
Assert.False(parsed.MapInt32Enum.ContainsKey(2));
962+
}
963+
964+
[Test]
965+
public void Enum_InvalidNumber_IgnoreUnknownFields()
966+
{
967+
// Even when ignoring unknown fields, fail for non-integer numeric values, because
968+
// they could *never* be valid.
969+
var parser = new JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true));
970+
string json = "{ \"singleForeignEnum\": 5.5 }";
971+
Assert.Throws<InvalidProtocolBufferException>(() => parser.Parse<TestAllTypes>(json));
972+
}
973+
927974
[Test]
928975
public void OneofDuplicate_Invalid()
929976
{

csharp/src/Google.Protobuf/JsonParser.cs

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22
// Protocol Buffers - Google's data interchange format
33
// Copyright 2015 Google Inc. All rights reserved.
44
//
@@ -219,8 +219,10 @@ private void MergeField(IMessage message, FieldDescriptor field, JsonTokenizer t
219219
}
220220
else
221221
{
222-
var value = ParseSingleValue(field, tokenizer);
223-
field.Accessor.SetValue(message, value);
222+
if (TryParseSingleValue(field, tokenizer, out var value))
223+
{
224+
field.Accessor.SetValue(message, value);
225+
}
224226
}
225227
}
226228

@@ -241,12 +243,14 @@ private void MergeRepeatedField(IMessage message, FieldDescriptor field, JsonTok
241243
return;
242244
}
243245
tokenizer.PushBack(token);
244-
object value = ParseSingleValue(field, tokenizer);
245-
if (value == null)
246+
if (TryParseSingleValue(field, tokenizer, out object value))
246247
{
247-
throw new InvalidProtocolBufferException("Repeated field elements cannot be null");
248+
if (value == null)
249+
{
250+
throw new InvalidProtocolBufferException("Repeated field elements cannot be null");
251+
}
252+
list.Add(value);
248253
}
249-
list.Add(value);
250254
}
251255
}
252256

@@ -276,8 +280,10 @@ private void MergeMapField(IMessage message, FieldDescriptor field, JsonTokenize
276280
return;
277281
}
278282
object key = ParseMapKey(keyField, token.StringValue);
279-
object value = ParseSingleValue(valueField, tokenizer);
280-
dictionary[key] = value ?? throw new InvalidProtocolBufferException("Map values must not be null");
283+
if (TryParseSingleValue(valueField, tokenizer, out object value))
284+
{
285+
dictionary[key] = value ?? throw new InvalidProtocolBufferException("Map values must not be null");
286+
}
281287
}
282288
}
283289

@@ -293,7 +299,15 @@ private static bool IsGoogleProtobufNullValueField(FieldDescriptor field)
293299
field.EnumType.FullName == NullValueDescriptor.FullName;
294300
}
295301

296-
private object ParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer)
302+
/// <summary>
303+
/// Attempts to parse a single value from the JSON. When the value is completely invalid,
304+
/// this will still throw an exception; when it's "conditionally invalid" (currently meaning
305+
/// "when there's an unknown enum string value") the method returns false instead.
306+
/// </summary>
307+
/// <returns>
308+
/// true if the value was parsed successfully; false for an ignorable parse failure.
309+
/// </returns>
310+
private bool TryParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer, out object value)
297311
{
298312
var token = tokenizer.Next();
299313
if (token.Type == JsonToken.TokenType.Null)
@@ -302,13 +316,17 @@ private object ParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer)
302316
// dynamically.
303317
if (IsGoogleProtobufValueField(field))
304318
{
305-
return Value.ForNull();
319+
value = Value.ForNull();
306320
}
307-
if (IsGoogleProtobufNullValueField(field))
321+
else if (IsGoogleProtobufNullValueField(field))
308322
{
309-
return NullValue.NullValue;
323+
value = NullValue.NullValue;
310324
}
311-
return null;
325+
else
326+
{
327+
value = null;
328+
}
329+
return true;
312330
}
313331

314332
var fieldType = field.FieldType;
@@ -327,7 +345,8 @@ private object ParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer)
327345
tokenizer.PushBack(token);
328346
IMessage subMessage = NewMessageForField(field);
329347
Merge(subMessage, tokenizer);
330-
return subMessage;
348+
value = subMessage;
349+
return true;
331350
}
332351
}
333352

@@ -337,18 +356,26 @@ private object ParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer)
337356
case JsonToken.TokenType.False:
338357
if (fieldType == FieldType.Bool)
339358
{
340-
return token.Type == JsonToken.TokenType.True;
359+
value = token.Type == JsonToken.TokenType.True;
360+
return true;
341361
}
342362
// Fall through to "we don't support this type for this case"; could duplicate the behaviour of the default
343363
// case instead, but this way we'd only need to change one place.
344364
goto default;
345365
case JsonToken.TokenType.StringValue:
346-
return ParseSingleStringValue(field, token.StringValue);
366+
if (field.FieldType != FieldType.Enum)
367+
{
368+
value = ParseSingleStringValue(field, token.StringValue);
369+
return true;
370+
}
371+
else
372+
{
373+
return TryParseEnumStringValue(field, token.StringValue, out value);
374+
}
347375
// Note: not passing the number value itself here, as we may end up storing the string value in the token too.
348376
case JsonToken.TokenType.Number:
349-
return ParseSingleNumberValue(field, token);
350-
case JsonToken.TokenType.Null:
351-
throw new NotImplementedException("Haven't worked out what to do for null yet");
377+
value = ParseSingleNumberValue(field, token);
378+
return true;
352379
default:
353380
throw new InvalidProtocolBufferException("Unsupported JSON token type " + token.Type + " for field type " + fieldType);
354381
}
@@ -694,18 +721,32 @@ private static object ParseSingleStringValue(FieldDescriptor field, string text)
694721
ValidateInfinityAndNan(text, float.IsPositiveInfinity(f), float.IsNegativeInfinity(f), float.IsNaN(f));
695722
return f;
696723
case FieldType.Enum:
697-
var enumValue = field.EnumType.FindValueByName(text);
698-
if (enumValue == null)
699-
{
700-
throw new InvalidProtocolBufferException($"Invalid enum value: {text} for enum type: {field.EnumType.FullName}");
701-
}
702-
// Just return it as an int, and let the CLR convert it.
703-
return enumValue.Number;
724+
throw new InvalidOperationException($"Use TryParseEnumStringValue for enums");
704725
default:
705726
throw new InvalidProtocolBufferException($"Unsupported conversion from JSON string for field type {field.FieldType}");
706727
}
707728
}
708729

730+
private bool TryParseEnumStringValue(FieldDescriptor field, string text, out object value)
731+
{
732+
var enumValue = field.EnumType.FindValueByName(text);
733+
if (enumValue == null)
734+
{
735+
if (settings.IgnoreUnknownFields)
736+
{
737+
value = null;
738+
return false;
739+
}
740+
else
741+
{
742+
throw new InvalidProtocolBufferException($"Invalid enum value: {text} for enum type: {field.EnumType.FullName}");
743+
}
744+
}
745+
// Just return it as an int, and let the CLR convert it.
746+
value = enumValue.Number;
747+
return true;
748+
}
749+
709750
/// <summary>
710751
/// Creates a new instance of the message type for the given field.
711752
/// </summary>

0 commit comments

Comments
 (0)