.NET
ISerializable
inheritance security
.NET 4
serialization

How can I implement ISerializable in .NET 4 without violating inheritance security rules?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If you really need ISerializable in .NET 4, the safe pattern is not just "implement GetObjectData and move on." You also need the correct serialization constructor, input validation during deserialization, and a clear inheritance story. In many cases, the simplest way to avoid inheritance-related security problems is to seal the class.

The Design Choice Comes First

Before writing code, decide whether the type should be inheritable.

  • If the type does not need inheritance, make it sealed.
  • If inheritance is required, use the standard protected serialization constructor and allow derived types to extend serialization explicitly.

Why this matters: custom serialization can expose object internals, and inheritance complicates who controls that data contract. A poorly designed base class can let derived classes deserialize the object into an invalid or unsafe state.

The Core ISerializable Pattern

A correct implementation has these pieces:

  1. the type is marked [Serializable]
  2. it implements ISerializable
  3. it defines a special deserialization constructor
  4. GetObjectData writes all required state
  5. the constructor validates what it reads back

Here is a small sealed example:

csharp
1using System;
2using System.Runtime.Serialization;
3
4[Serializable]
5public sealed class ApiCredential : ISerializable
6{
7    public string Name { get; }
8    public int Version { get; }
9
10    public ApiCredential(string name, int version)
11    {
12        Name = name ?? throw new ArgumentNullException(nameof(name));
13        Version = version;
14    }
15
16    private ApiCredential(SerializationInfo info, StreamingContext context)
17    {
18        if (info == null) throw new ArgumentNullException(nameof(info));
19
20        Name = info.GetString(nameof(Name))
21            ?? throw new SerializationException("Name is missing.");
22        Version = info.GetInt32(nameof(Version));
23
24        if (Version < 0)
25            throw new SerializationException("Version cannot be negative.");
26    }
27
28    public void GetObjectData(SerializationInfo info, StreamingContext context)
29    {
30        if (info == null) throw new ArgumentNullException(nameof(info));
31
32        info.AddValue(nameof(Name), Name);
33        info.AddValue(nameof(Version), Version);
34    }
35}

This pattern keeps the serialization contract local to one sealed type, which is the least risky option.

If the Class Must Be Inheritable

For an unsealed base class, the deserialization constructor should be protected, not private, so derived classes can participate correctly.

GetObjectData should also be virtual so a derived class can call the base implementation and then add its own values.

csharp
1using System;
2using System.Runtime.Serialization;
3
4[Serializable]
5public class PersonRecord : ISerializable
6{
7    public string Id { get; }
8
9    public PersonRecord(string id)
10    {
11        Id = id ?? throw new ArgumentNullException(nameof(id));
12    }
13
14    protected PersonRecord(SerializationInfo info, StreamingContext context)
15    {
16        if (info == null) throw new ArgumentNullException(nameof(info));
17        Id = info.GetString(nameof(Id))
18            ?? throw new SerializationException("Id is missing.");
19    }
20
21    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
22    {
23        if (info == null) throw new ArgumentNullException(nameof(info));
24        info.AddValue(nameof(Id), Id);
25    }
26}

A derived type then extends the contract explicitly:

csharp
1[Serializable]
2public sealed class CustomerRecord : PersonRecord
3{
4    public string DisplayName { get; }
5
6    public CustomerRecord(string id, string displayName) : base(id)
7    {
8        DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
9    }
10
11    private CustomerRecord(SerializationInfo info, StreamingContext context)
12        : base(info, context)
13    {
14        DisplayName = info.GetString(nameof(DisplayName))
15            ?? throw new SerializationException("DisplayName is missing.");
16    }
17
18    public override void GetObjectData(SerializationInfo info, StreamingContext context)
19    {
20        base.GetObjectData(info, context);
21        info.AddValue(nameof(DisplayName), DisplayName);
22    }
23}

This is the important inheritance rule: base and derived types must cooperate instead of each trying to own the full serialized state independently.

Practical Security Guidance

If your real concern is security, the strongest advice is broader than syntax:

  • validate everything during deserialization
  • keep invariants enforced in the serialization constructor
  • prefer sealed types unless extensibility is truly required
  • avoid custom binary serialization entirely for new designs when a safer serializer fits the problem

In modern .NET guidance, custom binary serialization is considered legacy territory. But if you are maintaining a .NET 4 codebase, the pattern above is the one to follow.

Common Pitfalls

The most common mistake is forgetting the special serialization constructor. Without it, deserialization cannot reconstruct the object correctly.

Another mistake is making an inheritable type but keeping serialization logic non-virtual or private in the wrong places. That breaks derived classes and encourages unsafe workarounds.

A third pitfall is deserializing fields without validation. ISerializable gives you control, which means you are also responsible for rejecting invalid state.

Summary

  • If possible, seal the class and keep the ISerializable contract local to that type.
  • Mark the type [Serializable], implement GetObjectData, and provide the special serialization constructor.
  • For base classes, use a protected serialization constructor and a virtual GetObjectData method.
  • Always validate deserialized values before accepting them as object state.
  • Treat ISerializable as legacy infrastructure and use it carefully in .NET 4 maintenance code.

Course illustration
Course illustration

All Rights Reserved.