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.