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

Configuration

This detector does not need any configuration.