Skip to content

Commit dc4e429

Browse files
Improve C# handling of Any JSON serialization/deserialization
An Any without a type URL is now serialized to JSON as an empty object. A JSON object without a type URL is now deserialized as an empty Any. A JSON object with an invalid type URL now throws InvalidProtocolBufferException. PiperOrigin-RevId: 753147779
1 parent f5bc2a3 commit dc4e429

File tree

5 files changed

+38
-4
lines changed

5 files changed

+38
-4
lines changed

‎conformance/failure_list_csharp.txt‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,4 @@ Recommended.Editions_Proto3.ValueRejectInfNumberValue.JsonOutput
3333
Recommended.Editions_Proto3.ValueRejectNanNumberValue.JsonOutput
3434
Required.Editions_Proto2.ProtobufInput.UnknownOrdering.ProtobufOutput
3535
Required.Editions_Proto3.ProtobufInput.UnknownOrdering.ProtobufOutput
36-
Required.*.JsonInput.AnyWithNoType.* # Failed to parse input or produce output.
37-
Required.*.JsonInput.AnyWktRepresentationWithEmptyTypeAndValue # Should have failed to parse, but didn't.
38-
Required.*.JsonInput.AnyWktRepresentationWithBadType # Should have failed to parse, but didn't.
3936

‎csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,13 @@ public void AnyUnknownType()
591591
Assert.Throws<InvalidOperationException>(() => JsonFormatter.Default.Format(any));
592592
}
593593

594+
[Test]
595+
public void AnyEmpty()
596+
{
597+
var any = new Any();
598+
AssertJson("{ }", JsonFormatter.Default.Format(any));
599+
}
600+
594601
[Test]
595602
[TestCase(typeof(BoolValue), true, "true")]
596603
[TestCase(typeof(Int32Value), 32, "32")]

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,17 @@ public void Any_UnknownType()
840840
[Test]
841841
public void Any_NoTypeUrl()
842842
{
843+
// Currently if we're missing the type URL (@type) we return an empty Any message
844+
// regardless of other fields. In the future we could potentially fail if there's no
845+
// type URL but other fields are present.
843846
string json = "{ \"foo\": \"bar\" }";
847+
Assert.AreEqual(new Any(), Any.Parser.ParseJson(json));
848+
}
849+
850+
[Test]
851+
public void Any_BadTypeUrl()
852+
{
853+
string json = "{ \"@type\": \"no-slash\" }";
844854
Assert.Throws<InvalidProtocolBufferException>(() => Any.Parser.ParseJson(json));
845855
}
846856

‎csharp/src/Google.Protobuf/JsonFormatter.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,13 @@ private void WriteAny(TextWriter writer, IMessage value, int indentationLevel) {
471471

472472
string typeUrl =
473473
(string)value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
474+
// If no type URL has been specified, just return an empty JSON object.
475+
if (typeUrl == "") {
476+
WriteBracketOpen(writer, ObjectOpenBracket);
477+
WriteBracketClose(writer, ObjectCloseBracket, false, indentationLevel);
478+
return;
479+
}
480+
474481
ByteString data =
475482
(ByteString)value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
476483
string typeName = Any.GetTypeName(typeUrl);

‎csharp/src/Google.Protobuf/JsonParser.cs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,9 +516,13 @@ private void MergeAny(IMessage message, JsonTokenizer tokenizer)
516516
tokens.Add(token);
517517
token = tokenizer.Next();
518518

519+
// If we get to the end of the object and haven't seen a type URL, just return.
520+
// (The message will be empty at this point.)
521+
// We could potentially make this more conservative, failing if there are
522+
// other properties but no type URL.
519523
if (tokenizer.ObjectDepth < typeUrlObjectDepth)
520524
{
521-
throw new InvalidProtocolBufferException("Any message with no @type");
525+
return;
522526
}
523527
}
524528

@@ -530,6 +534,15 @@ private void MergeAny(IMessage message, JsonTokenizer tokenizer)
530534
}
531535
string typeUrl = token.StringValue;
532536
string typeName = Any.GetTypeName(typeUrl);
537+
// If we don't find a slash, GetTypeName returns an empty string. An empty string can
538+
// never be a valid type name (whether that was through @type="", @type="blah" or
539+
// @type="blah/") so we fail. This is InvalidProtocolBufferException rather than
540+
// the InvalidOperationException used below, as it's the data that's invalid rather than
541+
// the context in which we're parsing it.
542+
if (typeName == "")
543+
{
544+
throw new InvalidProtocolBufferException("Invalid Any.@type value");
545+
}
533546

534547
MessageDescriptor descriptor = settings.TypeRegistry.Find(typeName);
535548
if (descriptor == null)

0 commit comments

Comments
 (0)