Type names ISerializable on its Implements clause but declares no GetObjectData
ID |
vbnet.correctness.implement_i_serializable_correctly |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Serialization |
Language |
VB.NET |
Description
Reports a class whose Implements clause names ISerializable, written either bare or
namespace-qualified, and which declares no GetObjectData member. A class that declares
Sub GetObjectData(…), or that implements the interface member under another name through an
Implements ISerializable.GetObjectData clause, is not reported. Only the presence of the
member is checked, not whether it writes every field of the type.
Rationale
ISerializable is a contract with the formatter: the type takes over its own serialization,
and GetObjectData is the half that decides what gets written. Leaving it out does not
compile in VB.NET when the interface is implemented directly, so the case that reaches
production is the one where the member was lost - renamed during a refactor, moved to a partial
file that was not carried over, or deleted with the field it wrote - or where the type inherits
a base implementation that no longer covers the fields the derived type added. The result is
the same either way: the formatter serializes whatever the inherited or empty implementation
provides, the fields introduced by this type are absent from the stream, and deserialization
returns an object whose state is silently partial. Nothing raises. A cart comes back with no
items, a session with no token, an audit trail with no entries, and the failure is attributed to
whatever consumes the object rather than to the round trip that dropped its state. Because the
write and the read sides are symmetric, a round-trip unit test written against the same version
of the type also passes.
The following code illustrates the pattern detected by this rule:
Namespace Acme.Sessions
' FLAGGED: Type names ISerializable on its Implements clause but declares no GetObjectData
<Serializable>
Public Class ShoppingCart
Implements ISerializable
Private items As New List(Of String)()
Public Sub Add(ByVal sku As String)
items.Add(sku)
End Sub
Remediation
Declare GetObjectData on the type, writing every field it owns with info.AddValue, and call
MyBase.GetObjectData(info, context) first when the base type also implements the interface so
the inherited state is written too. Pair it with the deserialization constructor
Protected Sub New(info As SerializationInfo, context As StreamingContext), which reads the
same names back, and keep the two in step whenever a field is added. If the type does not
actually need custom serialization, drop ISerializable and rely on <Serializable> with
<NonSerialized> on the fields to exclude.
' Before: the type claims ISerializable but nothing writes its state
<Serializable>
Public Class ShoppingCart
Implements ISerializable
Private items As New List(Of String)()
End Class
' After
<Serializable>
Public Class ShoppingCart
Implements ISerializable
Private items As New List(Of String)()
Public Sub GetObjectData(info As SerializationInfo, context As StreamingContext) _
Implements ISerializable.GetObjectData
info.AddValue("items", items)
End Sub
Protected Sub New(info As SerializationInfo, context As StreamingContext)
items = CType(info.GetValue("items", GetType(List(Of String))), List(Of String))
End Sub
End Class