Can I make Json.net deserialize a C# 9 record type with the "primary" constructor, as if it had [JsonConstructor]?

Viewed 10311

Using C# 9 on .NET 5.0, I have a bunch of record types, like this:

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}

As you might expect, they get serialized and sent elsewhere for handling. Senders call the two-argument constructor and get a new Id, but the deserializer needs to use the "primary" 3-argument constructor implied by the record declaration. I'm using Newtonsoft Json.NET and I sure wish this worked:

        var record = new SomethingHappenedEvent("roof", "caught fire");
        var json = JsonConvert.SerializeObject(record);
        var otherSideRecord = JsonConvert.DeserializeObject<SomethingHappenedEvent>(json);
        Assert.AreEqual(record, otherSideRecord);

Of course it doesn't. It throws JsonSerializationException. It can't find the right constructor because there are two, neither is a default zero-argument constructor, and neither is marked with JsonConstructorAttribute. My question is really "What options do I have for getting something similar?". This would be great:

[JsonConstructor]
public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}

But that tries to apply the attribute to the type, which is invalid. And this is a syntax error in C#, though apparently it works in F#.

public record SomethingHappenedEvent
[JsonConstructor]
    (Guid Id, object TheThing, string WhatHappened)
{
    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}

My current solution is to leave these types as plain classes and live with all the extra boilerplate. I'm also aware I can omit the custom constructor and make my callers generate the ids. This works because there's only one constructor for json.net to find. It's certainly terse! But I don't love repeating code at all the call sites, even if it is small in this case.

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened) { }

FWIW it sounds like System.Text.Json has the same limitation.

3 Answers

Firstly, you only have to do this when you create your own constructors. This is due to the fact that on instantiation it won't know which one to use.

Secondly, note that (by default) the deserializer will use the property and constructor names and overwrite the ones you omit in the actual constructor type. Furthermore, each parameter in the constructor must bind to an object property or field on deserialization. The formers can lead to subtle errors if you are not aware of them, however this is not limited solely to records.

All that aside, you had the attribute in the wrong place. In short, the attribute needs to be on the constructor.

Wildly contrived nonsensical example:

Given

public record TestRecord(Guid Id)
{
   [JsonConstructor]
   public TestRecord(object theThing, string whatHappened) : this(Guid.NewGuid())
   {
   }
}

Test

var record = new TestRecord(Guid.NewGuid());
var json = JsonConvert.SerializeObject(record,Formatting.Indented);
Console.WriteLine(json);
var otherSideRecord = JsonConvert.DeserializeObject<TestRecord>(json);

// note this paradoxically still works, because it has overwritten the ID
Console.WriteLine(record == otherSideRecord);

Ouput

{
  "Id": "2905cfaf-d13d-4df1-af83-e4dcde20d44f"
}
True

Note that the attribute also works with Text.Json

var json = JsonSerializer.Serialize(record);
var otherSideRecord = JsonSerializer.Deserialize<TestRecord>(json);

I ran across another option while experimenting. I'll leave it here in case it's useful to someone. You can convert the custom constructor(s) to factory method(s). This leaves only one constructor for the deserializer to find.

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    public static SomethingHappenedEvent Create(object theThing, string whatHappened)
        => new(Guid.NewGuid(), theThing, whatHappened);
}

It changes the syntax at the call sites. Just call Create instead of new:

var myEvent = SomethingHappenedEvent.Create("partyPeople", "gotDown");

Of course you could push the static factory method(s) to a separate factory object - that might be useful if your object construction has richer dependencies.

You can add a default constructor with the attribute. Json.Net will fill the properties.

If you want you can make it private so none of the other users of the class can use it by mistake. Json.Net will still find it:

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    [JsonConstructor]
    private SomethingHappenedEvent()
    { }

    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}
Related