Deserialization Method For OptionalField

ID

csharp.deserialization_method_for_optionalfield

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

attributes, reliability, serialization, versioning

Description

Reports a type that marks one or more fields [OptionalField] but declares no [OnDeserializing] or [OnDeserialized] handler. The optional field is skipped when the stream has no data for it, and nothing puts a value in its place.

Rationale

[OptionalField] exists for version tolerance. It tells the serializer that a stream written before the field existed is still readable: instead of failing on the missing member, the deserializer moves on. That is exactly the behaviour you want when a type gains a field and old data has to keep loading.

What it does not do is choose a value. The field is left at its type default — 0 for numbers, false for bool, null for anything by reference. The object is then handed back to the caller in a state that no constructor of the type could ever produce, and the type’s own invariants are broken before any of its code has run.

The deserialization callbacks are the mechanism the platform provides for this. [OnDeserializing] runs before the stream is read and can pre-seed defaults; [OnDeserialized] runs afterwards and can inspect what actually arrived and repair whatever is missing. A type with optional fields and neither callback has done half of a version migration: it accepts the old stream and then behaves as if the new field had been supplied.

The consequence lands far from the cause. Nothing throws while the graph is being read; the failure surfaces later, in whichever method first trusts the field, usually as an unexpected null or a total that is short by exactly the records loaded from old data.

partial types are not reported, because the handler may be declared in another part of the type in a file this rule does not examine.

using System;
using System.Runtime.Serialization;

[Serializable]
public class OldReport
{
    private string title;

    [OptionalField]
    private string author;                                 // FLAW — nothing ever sets it

    public string Describe() => title + " by " + author.Trim();
}

[Serializable]
public class NewReport
{
    private string title;

    [OptionalField]
    private string author;                                 // OK — the handler supplies a value

    [OnDeserialized]
    private void AfterRead(StreamingContext context)
    {
        author ??= "unknown";
    }
}

Remediation

Add a deserialization callback and give the optional field a value there:

[OnDeserialized]
private void AfterRead(StreamingContext context)
{
    author ??= "unknown";
}

Assign in [OnDeserialized] when the correct value depends on what the stream contained, and in [OnDeserializing] when a plain default is enough and you want it in place before reading starts.

If the type default really is the intended value for old data, say so explicitly by assigning it in a callback anyway. Reviewers cannot tell a considered default from a forgotten one, and the next field added to the type will inherit the same ambiguity. Removing [OptionalField] is the other valid answer, but it makes old streams fail to load, so treat that as a deliberate compatibility break.