Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 238
solublefish Avatar asked Dec 19 '20 01:12

solublefish


People also ask

How do I deserialize JSON in .NET core?

NET objects (deserialize) A common way to deserialize JSON is to first create a class with properties and fields that represent one or more of the JSON properties. Then, to deserialize from a string or a file, call the JsonSerializer. Deserialize method.

How deserialize JSON works C#?

In Deserialization, it does the opposite of Serialization which means it converts JSON string to custom . Net object. In the following code, it creates a JavaScriptSerializer instance and calls Deserialize() by passing JSON data. It returns a custom object (BlogSites) from JSON data.

Can JSON parse C#?

Parse() method is an object class method and this method is used to parse the JSON string into the objects of C#. Based on the key value it parses the data of string and then it retrieves the data by using the key values.

Does .NET support JSON?

JSON.Net is well supported and it appears that Microsoft intend to adopt it themselves "We on the web team will be including JSON.NET as the default JSON Serializer in Web API when it releases, so that'll be nice." from hanselman.com/blog/… Just be aware of the embedded library for JSon serializing's performance in .


2 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);
like image 150
TheGeneral Avatar answered Oct 17 '22 05:10

TheGeneral


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.

like image 23
solublefish Avatar answered Oct 17 '22 04:10

solublefish