3

I am using YamlDotNet and I wanted to use records for my deserialized YAML. However, I get an error like this when I try to do so:

Unhandled exception. (Line: 2, Col: 3, Idx: 9) - (Line: 2, Col: 3, Idx: 9): Exception during deserialization

From further experimentation, I discovered record structs, which work completely fine. Regular structs work and so do classes with get and init properties. Clearly I do not know much about records in C#. Out of curiosity, does anybody know what it is about records that prevents them from being deserialized to by YamlDotNet?

BlueStaggo
  • 171
  • 1
  • 12
  • Do you have any detailed exception messages (perhaps in the `InnerException`)? – canton7 Oct 24 '22 at 14:59
  • Records don't have settable properties but can only be initialized through a constructor call -- which requires that data is matched positionally, which is beyond the capabilities of most libraries. – Jeroen Mostert Oct 24 '22 at 15:00
  • @JeroenMostert You can have records with settable properties just fine – canton7 Oct 24 '22 at 15:01
  • @canton7: OK, my bad -- class records don't have settable properties *by default*, when declared in the usual way. (Whereas record structs *do* if not explicitly declared `readonly`, which explains the difference.) – Jeroen Mostert Oct 24 '22 at 15:02
  • @canton7 I do get something like `System.MissingMethodException : Cannot dynamically create an instance of type 'RecordType'. Reason: No parameterless constructor defined.` – BlueStaggo Oct 24 '22 at 15:03
  • And init-only properties can be set through reflection (which was intentional, so as not to break serializers) – canton7 Oct 24 '22 at 15:03
  • @BlueStaggo Your record needs to have a parameterless constructor – canton7 Oct 24 '22 at 15:04
  • My best guess is that the deserializer tries to create a class through a default constructor then it sets the properties. I guess this cannot be done with records. – BlueStaggo Oct 24 '22 at 15:33
  • @BlueStaggo You can have a record with a parameterless constructor, e.g. [here](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA+tJQCYAIBKAFAJYB2ALrgBoCUAsAFADejubuAAgMwGE24hc5ABbEAzoS78muAL6NZQA) – canton7 Oct 25 '22 at 08:01

1 Answers1

0

tl;dr you'll need a simple workaround, because it's not supported out-of-the-box, see https://github.com/aaubry/YamlDotNet/issues/571

You need to have paremeter-less (0-arg) constructor in your record for current (circa 13.x) YamlDotNet versions to be able to deserialize. That c-tor will be used solely to create the record in its pre-deserialization state, so you can provide it with normally invalid/unwanted values, like null or other defaults. YamlDotNet will then use "magical" (reflection-based) setting methods anyway, and you can check for those invalid/unwanted values later to verify if the deserialized data actually has everything you need to have a complete record.

E.g.

public record Config(
    string someParam,
    string otherParam,
    string yetAnotherParam
) {
    public Config() : this(null, null, null) {
    }

    public void Validate() {
        if (someParam == null || otherParam == null || yetAnotherParam == null) {
            throw new ArgumentNullException("not all required fields were deserialized properly", (Exception) null);
        }
    }

}

will work just fine, and then you can just call Validate() straight after deserialization if you want (yes, it's not as effective as library-level support for this, but still).

Alternatively (although I don't really advise to do it), you can e.g.:

  • deserialize YAML to object using YamlDotNet,
  • serialize that object to JSON via System.Text.Json,
  • and finally deserialize that JSON to your target record (via System.Text.Json as well)

It's both terrible for performance and would require even more obfuscation of the logic in code, but at least can be hidden in a helper method etc.

spamove
  • 345
  • 3
  • 12