Search code examples
c#deserializationc#-10.0

C# fields: mandatory initialization but still accessible to deserializer


I'm facing a dilemma that's very common in C# (and other OO languages) where I'm trying to meet 3 competing requirements :

  1. I absolutely don't want the end-user of my class to forget to initialize some of the fields, at construction time.
  2. I want a deserializer to be able to instantiate that class and populate the fields without silently being "blocked" by a private setter.
  3. I don't want any non-initialized fields left hanging (e.g. a list has to be constructed, no loose null in there!)

Note:

This is NOT the same question as this old one : How to make a property required in c#? for two reasons :

  1. It doesn't deal with requirement #2 (expose fields for deserialization)
  2. It's 10 years old, and C# has evolved a lot since then.

So, let's say that this is my class :

public class MyClass {
     public bool Field1 {get; set;}
     public IEnumerable<bool> Field2 {get; set;} = default!; 
}

Note : don't obsess over the = default! bit, here I'm just making the compiler happy (it detects non-initialized fields) to move on to the core of the issue.

if I needed to meet requirement #1 only then I would NOT do this :

var o = new MyClass() {
             Field1 = true
             // Uh oh, I forgot Field2
        };

Instead I would do this :

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable<bool> Field2 {get; private set;}

     public MyClass(bool field1, IEnumerable<bool> field2) {
         Field1 = field1;
         Field2 = field2;
     } 
}

For requirement #3 only I would do this :

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable<bool> Field2 {get; private set;} = new List<bool>();

     public MyClass(bool field1) {
         Field1 = field1;
     } 
}

// ...

c.Field2.AddRange(...);

I would NOT do this because despite protecting the fields from tampering, it does not guarantee requirement #1 :

public class MyClass {
     public bool Field1 {get; init;}
     public IEnumerable<bool> Field2 {get; init;} = new List<bool>();
}

But now I have to fullfil requirement #2 . That's a problem because the deserializer cannot populate protected or private fields.

Which means that this would NOT work, as Field1 would remain false : (note: we're assuming that the deserializer is properly configured : no upper-case/lower-case nonsense or whatnot)

string json = @"{ ""Field1"": true, ""Field2"": [] }";
var o = JsonSerializer.Deserialize<MyClass>(json);
Assert(o.Field1 == true); // fails

The only solution I'm aware of to fulfill all 3 requirements is a constructor that has ALL the fields :

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable<bool> Field2 {get; private set;} = new List<bool>();

     [JsonConstructor] // <-- to make it air-tight!
     public MyClass(bool field1, IEnumerable<bool> field2) {
         Field1 = field1;
         Field2 = field2;
     } 
}

string json = @"{ ""Field1"": true, ""Field2"": [] }";
var o = JsonSerializer.Deserialize<MyClass>(json);
Assert(o.Field1 == true); // succeeds

even that solution is not perfect, because if I add a field "Field3" to the class then I might forget to add it to the exhaustive constructor, and the deserialization would silently ignore it. Fail!

My question :

Is there an elegant way of achieving this in modern C#? Many answers about this topic are 10+ years old. C# has made a ton of progress in every direction since then. Ideally, I'd like an answer for .Net6 (C# 10) AND possibly an answer for more recent C# (C# 11+)


Solution

  • For C# 11 - there is feature introduced to specifically support such scenarios - required modifier which allows your to do something like (also note the usage of init keyword which declares an init-only setter which allows to assign a value to the property only during object construction):

    public class MyClass 
    {
         public required bool Field1 {get; init;}
         public required IEnumerable<bool> Field2 {get; init;}
    }
    

    The last option can also be reduced to using records (available since C# 9):

    public record MyClass(bool Field1, IEnumerable<bool> Field2);
    

    P.S.

    Note that required modifier is also considered by System.Text.Json, so marking property with it will make the corresponding JSON property required in the JSON payload.