0

System.Text.Json.JsonSerializer.Serialize is a set of overloads that serialize a C# object into json.

The non-generic overloads all share three parameters - object? value which is the object to serialize; System.Text.Json.JsonSerializerOptions? options, which allows configuring the serializer with respect to all kinds of choices, and Type inputType, which is what this question is about.

inputType is merely described as "The type of the value to convert." However, what does that actually mean and do? Is there a meaningful difference between typeof(object) in this context and value.GetType()?

I peeked into the code, but it quickly became clearly this isn't a simple matter; the type helps resolve a JsonTypeInfo, but e.g. typeof(object) is special-cased there.

I did a few quick-and dirty benchmarks:

using System.Security.Cryptography;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<JsonBench>();

sealed record Bla(string Foo, int Bar);

public class JsonBench
{
    readonly Bla value = new Bla("a", 2);
    
    [Benchmark]
    public string WithGenerics() => JsonSerializer.Serialize(value);

    [Benchmark]
    public string WithGetType() => JsonSerializer.Serialize(value, value.GetType());

    [Benchmark]
    public string WithObjectType() => JsonSerializer.Serialize(value, typeof(object));

    readonly Type cachedObject = typeof(object), cachedBla = typeof(Bla);

    [Benchmark]
    public string WithCachedGetType() => JsonSerializer.Serialize(value, cachedBla);
    
    [Benchmark]
    public string WithCachedObjectType() => JsonSerializer.Serialize(value, cachedObject);
}

...and for small objects there appears to be very slight (on the order of 10ns) overhead from using typeof(object). Is that it? Are there corner cases where this is more? If using value.GetType() is reliably faster... why does this choice even exist all?

In short: I'm not sure I understand the purpose of this Type inputType parameter.

Can anybody clarify what this is actually for?

Eamon Nerbonne
  • 47,023
  • 20
  • 101
  • 166
  • 1
    Well, you also can `typeof(Bla)`, right? Which would then basically be the same as the Generic overload, I guess. And then it's probably also compile time vs runtime type resolution? Not sure abut that last one, though. – Fildor Jan 27 '23 at 15:49
  • ^^ You benchmarked with one single type, too. I guess results for `WithGenerics` and `WithGetType` will differ from the current ones if you tried many different types. – Fildor Jan 27 '23 at 15:55
  • @Fildor Using 2 types including within the same benchmark method does not affect the benchmark outcome; generics are fastest, but GetType is faster than object type. But most importantly: the differences are quite small, even in the likely worst case of many calls with tiny objects. – Eamon Nerbonne Jan 27 '23 at 16:42
  • There is also the polymorphic types issues described in dbc's answer. So, it's not only a question of performance but also behavior. – Fildor Jan 27 '23 at 16:44

1 Answers1

2

As stated in the docs, the inputType argument of JsonSerializer.Serialize(Object value, Type type, JsonSerializerOptions? options = default) specifies:

The type of the value to convert.

Specifically, inputType defines the type to use when determining the serialization contract for the incoming value:

  1. typeof(object) - The actual, concrete type of the incoming value is used.

  2. value.GetType() - Once again the actual, concrete type of the incoming value is used.

  3. Some other System.Type that value inherits from or implements -- this type will be used instead of the actual, concrete type.

E.g., consider the following type hierarchy:

public class Base
{
    public string BaseProperty { get; set; }
}

public class Derived : Base
{
    public string DerivedProperty { get; set; }
}

Then if you serialize an instance of Derived using typeof(Base):

var value = new Derived { BaseProperty = "hello", DerivedProperty = "there" };
var json = JsonSerializer.Serialize(value, typeof(Base));

Then the serializer will only serialize the properties of the specified base type Base:

{
  "BaseProperty" : "hello"
}

Demo here. You will get the same result if you do JsonSerializer.Serialize<Base>(value).

Since your sealed record Bla(string Foo, int Bar); does not have any derived or base types other than typeof(object), your benchmarks can't surface the third case.[1]

This is discussed in How to serialize properties of derived classes with System.Text.Json:

In versions prior to .NET 7, System.Text.Json doesn't support the serialization of polymorphic type hierarchies. For example, if a property's type is an interface or an abstract class, only the properties defined on the interface or abstract class are serialized, even if the runtime type has additional properties. The exceptions to this behavior are explained in this section.

...

To serialize the properties of the derived type... use one of the following approaches:

  • Call an overload of Serialize that lets you specify the type at run time.

  • Declare the object to be serialized as object.

Now, in .NET 7 polymorphic type hierarchies are supported, however if you don't mark your types with JsonDerivedTypeAttribute or use some other mechanism to inform the serializer of the expected derived types, the behavior falls back to the .NET 6 behavior.

Related questions:


[1] Eamon Nerbonne comments:

incidentally, that sealed record Bla does have base types, and those have the expected useless behavior when used here - JsonSerializer(new Bla("",1), typeof(IEquatable<Bla>)) == "{}".

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Right, so the small but consistent perf difference that is techniclly explainable by looking at the source code between object and concrete type is simply a spurious implementation detail? – Eamon Nerbonne Jan 27 '23 at 16:47
  • @EamonNerbonne - not clear, there might be some overhead to validate that `inputType.IsAssignableFrom(value.GetType()`, there might be some overhead due to contract creation the first time the serializer encounters your `Bla` type, there might be some overhead due to warmup JIT optimization. Do you run the benchmark once, or 10000 times and average? Or you might be encountering some measurement errors of the sort described by Eric Lippert here: https://ericlippert.com/2013/05/14/benchmarking-mistakes-part-one/ – dbc Jan 27 '23 at 16:51
  • Or it might just be due to some implementation peculiarity within MSFT's code. – dbc Jan 27 '23 at 16:59
  • It looks like an implementation quirk; I ran both handrolled benchmarks with 1000 iterations 10000 times to compute the median, but also the included benchmark.net implementation which does warmup runs. I reordered execution multiple times; and grouped it sometimes with 1 type, sometimes with 2 - none of that seems to matter; the difference is consistently a little less than 10ns per serialization. Which is odd, given they seem semantically identical, but it's hardly an impactful difference, merely a confusing one! – Eamon Nerbonne Jan 27 '23 at 18:36
  • 1
    incidentally, that sealed record Bla does have base types, and those have the expected useless behavior when used here - JsonSerializer(new Bla("",1), typeof(IEquatable)) == "{}". – Eamon Nerbonne Jan 27 '23 at 18:40