Serialization: JSON and XML

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Serialization turns objects into text or bytes that can be stored or sent, and deserialization turns them back. .NET offers several serializers with different promises: System.Text.Json for JSON, XmlSerializer and DataContractSerializer for XML, and the lower-level XmlReader, XmlWriter, XmlDocument and JsonDocument for documents you navigate yourself. Another serializer, BinaryFormatter, is gone: since .NET 9 its implementation always throws, and this page explains why and what replaces it. It was written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and checked on 2026-10-19. Every program was built with Option Strict On and TreatWarningsAsErrors and run in a Linux container; the project root namespace is ShelfDocs, which appears in some exception messages because the shelf types are nested in the example modules. Serialization in Visual Basic has some limits that C# does not have, mainly the unusable Utf8JsonReader type and the C#-only source generator; both are shown with real compiler output and a workaround that keeps the code in Visual Basic.

Reading and writing the files that hold serialized data is on Files, Directories and Streams. LINQ to XML and the XML literals that build an XDocument from inline XML are on XML Literals and Axis Properties. Attributes in general are on Attributes, Caller Info and Reflection, and the C# view is on Attributes and Reflection in the C# reference. Web services that return JSON are on Web, HTTP and Services, and the security view of untrusted input is continued on Security and Cryptography.

Choosing a serializer

Serializer Format and strengths Limits

System.Text.Json (JsonSerializer)

JSON, fast, built into .NET, strict by default, polymorphism and constructors supported, streams and async

Public properties only by default; cannot be extended with converters from Visual Basic; the source generator is C#-only

XmlSerializer

XML with full control of element and attribute names through attributes; the format of SOAP-era and many industry schemas

Public types with parameterless constructors only; no dictionaries; one class hierarchy needs explicit declarations

DataContractSerializer

XML (and JSON through DataContractJsonSerializer); opt-in members, private members allowed, no parameterless constructor needed, versioning support, WCF heritage

Strict element order; verbose namespaces; not for hand-written XML schemas

XmlReader, XmlWriter, XmlDocument, XDocument

You read and write the document yourself; XPath and LINQ to XML

More code, no object mapping

JsonDocument, JsonNode, Utf8JsonWriter

JSON you navigate or build yourself

Utf8JsonReader cannot be used from Visual Basic

BinaryFormatter, SoapFormatter, NetDataContractSerializer

Binary object graphs of any type

Removed: they throw PlatformNotSupportedException; insecure by design

Newtonsoft.Json and other packages

Third-party libraries with their own features and update schedule

Outside the scope of this reference

System.Text.Json basics

JsonSerializer.Serialize(value) produces compact JSON, and JsonSerializer.Deserialize(Of T)(text) creates a T from it. The defaults are strict and predictable: properties use their declared names; only public properties with public getters and setters are written and read; public fields are ignored; enums are numbers; dates use ISO 8601; unknown JSON members are ignored; property names are case-sensitive on input; comments, trailing commas and quoted numbers are errors. JsonSerializerOptions changes each of those, and JsonSerializerDefaults.Web selects the usual web choices (camelCase names, case-insensitive reading, numbers accepted from strings). Create one options instance and reuse it: it caches type metadata, so building a new instance per call is slow.

Option Strict On
Imports System.Text.Json
Imports System.Text.Json.Serialization

Module S01
    Public Enum LoanState
        Available
        OnLoan
        Reserved
    End Enum

    Public Class Book
        Public Property Isbn As String = ""
        Public Property Title As String = ""
        Public Property Author As String = ""
        Public Property Year As Integer
        Public Property Price As Decimal
        Public Property State As LoanState
        Public Property DueOn As Date?
        Public Property Tags As New List(Of String)()
        Public ReadOnly Property Label As String
            Get
                Return Title & " (" & Year.ToString() & ")"
            End Get
        End Property
        Public ShelfMark As String = "A-12"                ' a public field
        Private ReadOnly Secret As String = "not exported" ' a private field
    End Class

    Sub Main()
        Dim dune As New Book With {
            .Isbn = "978-0-441-17271-9", .Title = "Dune", .Author = "Frank Herbert", .Year = 1965,
            .Price = 9.99D, .State = LoanState.OnLoan, .DueOn = New Date(2026, 2, 3, 0, 0, 0)}
        dune.Tags.Add("sci-fi")
        dune.Tags.Add("classic")

        ' Defaults: compact, property names as declared, enums as numbers, fields and private members left out.
        Console.WriteLine(JsonSerializer.Serialize(dune))

        ' Options: indentation, camelCase names, enums as text, nulls omitted.
        Dim options As New JsonSerializerOptions With {
            .WriteIndented = True,
            .PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            .DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            .IncludeFields = True}
        options.Converters.Add(New JsonStringEnumConverter(JsonNamingPolicy.CamelCase))
        Dim text As String = JsonSerializer.Serialize(dune, options)
        Console.WriteLine(text)

        ' Round trip.
        Dim back As Book = JsonSerializer.Deserialize(Of Book)(text, options)
        Console.WriteLine($"Back: {back.Label}, {back.State}, due {back.DueOn:yyyy-MM-dd}, tags {String.Join("/", back.Tags)}, shelf mark {back.ShelfMark}")

        ' Deserialization is case-sensitive unless told otherwise, and unknown members are ignored.
        Dim lowerCase As String = "{""isbn"":""1"",""title"":""Emma"",""unknown"":true}"
        Console.WriteLine($"Case-sensitive: Title=[{JsonSerializer.Deserialize(Of Book)(lowerCase).Title}]")
        Dim insensitive As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True}
        Console.WriteLine($"Case-insensitive: Title=[{JsonSerializer.Deserialize(Of Book)(lowerCase, insensitive).Title}]")
        Console.WriteLine($"Web defaults: {JsonSerializer.Serialize(New Book With {.Title = "Emma"}, New JsonSerializerOptions(JsonSerializerDefaults.Web)).Substring(0, 44)}...")

        ' Collections and dictionaries.
        Dim prices As New Dictionary(Of String, Decimal) From {{"Dune", 9.99D}, {"Emma", 7.5D}}
        Console.WriteLine(JsonSerializer.Serialize(prices))
        Dim list As List(Of Book) = JsonSerializer.Deserialize(Of List(Of Book))("[{""Title"":""A""},{""Title"":""B""}]")
        Console.WriteLine($"List of {list.Count}: {String.Join(",", list.Select(Function(b) b.Title))}")
        Console.WriteLine($"Literal null deserializes to Nothing: {JsonSerializer.Deserialize(Of Book)("null") Is Nothing}")

        ' Bad input throws JsonException with the position.
        Try
            JsonSerializer.Deserialize(Of Book)("{""Year"": ""nineteen""}")
        Catch ex As JsonException
            Console.WriteLine($"JsonException at {ex.Path}, line {ex.LineNumber}, byte {ex.BytePositionInLine}")
        End Try
        Try
            JsonSerializer.Deserialize(Of Book)("{""Title"": ""Dune"",}")
        Catch ex As JsonException
            Console.WriteLine($"Trailing comma: {ex.GetType().Name}")
        End Try
        Dim lenient As New JsonSerializerOptions With {.AllowTrailingCommas = True, .ReadCommentHandling = JsonCommentHandling.Skip}
        Console.WriteLine($"Lenient parse: {JsonSerializer.Deserialize(Of Book)("{""Title"": ""Dune"", // note" & vbLf & "}", lenient).Title}")
    End Sub
End Module
{"Isbn":"978-0-441-17271-9","Title":"Dune","Author":"Frank Herbert","Year":1965,"Price":9.99,"State":1,"DueOn":"2026-02-03T00:00:00","Tags":["sci-fi","classic"],"Label":"Dune (1965)"}
{
  "isbn": "978-0-441-17271-9",
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965,
  "price": 9.99,
  "state": "onLoan",
  "dueOn": "2026-02-03T00:00:00",
  "tags": [
    "sci-fi",
    "classic"
  ],
  "label": "Dune (1965)",
  "shelfMark": "A-12"
}
Back: Dune (1965), OnLoan, due 2026-02-03, tags sci-fi/classic, shelf mark A-12
Case-sensitive: Title=[]
Case-insensitive: Title=[Emma]
Web defaults: {"isbn":"","title":"Emma","author":"","year"...
{"Dune":9.99,"Emma":7.5}
List of 2: A,B
Literal null deserializes to Nothing: True
JsonException at $.Year, line 0, byte 19
Trailing comma: JsonException
Lenient parse: Dune

The Visual Basic habits that matter here are small. A class made of auto-implemented properties (Public Property Title As String = "") is exactly what the serializer wants, including property initializers such as Tags As New List(Of String)(); the deserializer needs a public parameterless constructor unless it is told which constructor to use (next section). A ReadOnly property is written (Label in the output) and skipped on input. A public Field (the ShelfMark in the example) is ignored until IncludeFields or <JsonInclude> is used, a private field never appears, and the Date? property becomes null or an ISO string.

JsonSerializerOptions property Effect

WriteIndented

Pretty-printed output with two-space indentation.

PropertyNamingPolicy, DictionaryKeyPolicy

JsonNamingPolicy.CamelCase, SnakeCaseLower, KebabCaseLower and the others rename properties or dictionary keys.

PropertyNameCaseInsensitive

Match JSON names to properties regardless of case when reading.

DefaultIgnoreCondition

WhenWritingNull or WhenWritingDefault omit empty values when writing.

IncludeFields

Include public fields as well as properties.

Converters

Add JsonStringEnumConverter to write enums as names; custom converters cannot be written in Visual Basic (see below).

AllowTrailingCommas, ReadCommentHandling

Accept JSON that has trailing commas or comments, which strict JSON does not allow.

NumberHandling

AllowReadingFromString, WriteAsString, AllowNamedFloatingPointLiterals.

UnmappedMemberHandling

Skip (default) or Disallow.

MaxDepth

Maximum nesting depth when reading and writing; the default is 64.

ReferenceHandler

Preserve or IgnoreCycles for object graphs that contain cycles; without them a cycle is an exception.

TypeInfoResolver

Where type metadata comes from: reflection by default, a generated context when you supply one.

Encoder

By default non-ASCII and some HTML-sensitive characters are written as escapes; JavaScriptEncoder.UnsafeRelaxedJsonEscaping writes more characters literally and should not be used for HTML output.

Bad input throws JsonException with Path, LineNumber and BytePositionInLine, which is what to log. A document whose root is the literal null deserializes to Nothing, so always check the result before using it.

Attributes, constructors and surrogate properties

Attributes on the properties override the options for one member. The Visual Basic syntax is <JsonPropertyName("isbn")> before the property, several attributes may share a line separated by commas or sit on separate lines, and a named argument uses :=.

Attribute Effect

<JsonPropertyName("x")>

Name in JSON.

<JsonIgnore>, <JsonIgnore(Condition:=...)>

Never write/read, or skip when Null/default.

<JsonInclude>

Include a field or a property with a non-public accessor.

<JsonPropertyOrder(n)>

Position in the output (lower first, default 0).

<JsonRequired>

The member must be present in the input; absent means JsonException.

<JsonConstructor>

The constructor to use when there is no parameterless one; parameters match JSON names, case-insensitively.

<JsonExtensionData>

A Dictionary(Of String, JsonElement) (or JsonObject) that receives members with no matching property and writes them back.

<JsonConverter(GetType(...))>

Use a specific converter; see the next section for why Visual Basic cannot define one.

<JsonPolymorphic>, <JsonDerivedType>

Opt in to type discriminators for a base class (see the polymorphism section).

<JsonNumberHandling>

Per-member number handling.

Option Strict On
Imports System.Text.Json
Imports System.Text.Json.Serialization

Module S02
    Public Structure Isbn
        Public Sub New(value As String)
            Digits = value.Replace("-", "")
        End Sub
        Public ReadOnly Property Digits As String
        Public Overrides Function ToString() As String
            Return Digits
        End Function
    End Structure

    Public Class Loan
        ' Utf8JsonReader cannot be named in Visual Basic, so a custom JsonConverter cannot be written here.
        ' A string surrogate property keeps the JSON shape without one.
        <JsonIgnore>
        Public Property Isbn As Isbn

        <JsonPropertyName("isbn")>
        Public Property IsbnText As String
            Get
                Return Isbn.Digits
            End Get
            Set(value As String)
                Isbn = New Isbn(value)
            End Set
        End Property

        <JsonPropertyName("member")>
        <JsonPropertyOrder(-1)>
        Public Property Member As String = ""

        <JsonIgnore>
        Public Property InternalNote As String = ""

        <JsonIgnore(Condition:=JsonIgnoreCondition.WhenWritingDefault)>
        Public Property Renewals As Integer

        <JsonInclude>
        Public DueOn As Date

        <JsonExtensionData>
        Public Property Extra As Dictionary(Of String, JsonElement)
    End Class

    ' An immutable class: the constructor is chosen with an attribute, parameters match properties by name.
    Public Class Author
        <JsonConstructor>
        Public Sub New(name As String, born As Integer)
            Me.Name = name
            Me.Born = born
        End Sub

        Public ReadOnly Property Name As String
        Public ReadOnly Property Born As Integer
    End Class

    Public Class Member
        <JsonRequired>
        Public Property Card As String = ""
        Public Property Name As String = ""
    End Class

    Sub Main()
        Dim loan As New Loan With {
            .Isbn = New Isbn("978-0-441-17271-9"), .Member = "ada", .InternalNote = "do not export",
            .DueOn = New Date(2026, 2, 3, 0, 0, 0)}
        Dim json As String = JsonSerializer.Serialize(loan)
        Console.WriteLine(json)

        Dim input As String = "{""member"":""bob"",""isbn"":""9780140439519"",""DueOn"":""2026-03-01T00:00:00"",""renewals"":2,""channel"":""web"",""score"":4.5}"
        Dim parsed As Loan = JsonSerializer.Deserialize(Of Loan)(input)
        Console.WriteLine($"member={parsed.Member}, isbn={parsed.Isbn}, renewals={parsed.Renewals} (property name is Renewals, JSON said renewals)")
        Console.WriteLine($"Extension data: {String.Join(", ", parsed.Extra.Select(Function(kv) kv.Key & "=" & kv.Value.ToString()))}")
        Console.WriteLine($"Round trip keeps extension data: {JsonSerializer.Serialize(parsed)}")

        ' Constructor-based deserialization.
        Dim author As Author = JsonSerializer.Deserialize(Of Author)("{""Name"":""Jane Austen"",""Born"":1775}")
        Console.WriteLine($"{author.Name}, born {author.Born}")

        ' Required members.
        Try
            JsonSerializer.Deserialize(Of Member)("{""Name"":""Ada""}")
        Catch ex As JsonException
            Console.WriteLine($"Missing required member: {ex.Message}")
        End Try

        ' A structure with no public properties or fields has nothing to read from the JSON text.
        Try
            Console.WriteLine($"Isbn without a converter: {JsonSerializer.Serialize(New Isbn("1-2"))}")
            JsonSerializer.Deserialize(Of Isbn)("""x""")
        Catch ex As JsonException
            Console.WriteLine($"Deserializing a string into a structure: {ex.GetType().Name}")
        End Try
    End Sub
End Module
{"member":"ada","isbn":"9780441172719","DueOn":"2026-02-03T00:00:00"}
member=bob, isbn=9780140439519, renewals=0 (property name is Renewals, JSON said renewals)
Extension data: renewals=2, channel=web, score=4.5
Round trip keeps extension data: {"member":"bob","isbn":"9780140439519","DueOn":"2026-03-01T00:00:00","renewals":2,"channel":"web","score":4.5}
Jane Austen, born 1775
Missing required member: JSON deserialization for type 'ShelfDocs.S02+Member' was missing required properties including: 'Card'.
Isbn without a converter: {"Digits":"12"}
Deserializing a string into a structure: JsonException

Read the output: the member property came first because of <JsonPropertyOrder(-1)>; InternalNote is not written; Renewals was left out because it is zero and the condition is WhenWritingDefault; the public field DueOn is written because of <JsonInclude>; in the input, the member renewals (lowercase) does not match the property Renewals (the matching is case-sensitive), so it lands in the extension data together with the other unknown members, and a round trip writes them back. The Isbn structure has no useful public members to describe it, so the serialized form of a structure that hides its data would be {"Digits":"..."}; the class avoids that by keeping the structure out of the JSON with <JsonIgnore> and exposing a string surrogate property, IsbnText, renamed to isbn. A surrogate property is the Visual Basic substitute for a converter whenever the value has a natural text form.

Custom converters: Utf8JsonReader cannot be named in Visual Basic

A converter is the System.Text.Json extension point: a class that inherits JsonConverter(Of T) and overrides Read(ByRef reader As Utf8JsonReader, ...) and Write(writer As Utf8JsonWriter, ...). Utf8JsonReader is a ref struct, a kind of type that the Visual Basic compiler does not support: it treats such a type as obsolete with the message below, so any signature, local variable or field of that type is an error.

Option Strict On
Imports System.Text
Imports System.Text.Json

Module S03b
    Sub Main()
        Dim bytes As Byte() = Encoding.UTF8.GetBytes("[1,2,3]")
        Dim reader As New Utf8JsonReader(bytes)
        Do While reader.Read()
            Console.WriteLine(reader.TokenType)
        Loop
    End Sub
End Module
S03b.vb(8,27): error BC30668: 'Utf8JsonReader' is obsolete: 'Types with embedded references are not supported in this version of your compiler.'.

This means that a Visual Basic project cannot implement JsonConverter(Of T) (its Read method has such a parameter), cannot write a JsonConverterFactory, and cannot read JSON token by token. Utf8JsonWriter is a normal class and works, as does everything that goes through JsonSerializer, JsonDocument, JsonElement and JsonNode. The options are, in order of preference:

  • a surrogate property of a simple type, as in the previous section (no extra project);

  • a generic converter written once in C# in a small class library, which the Visual Basic code configures with lambdas (below);

  • reading the document with JsonDocument or JsonNode and building the objects by hand.

Source generation and Visual Basic

The System.Text.Json source generator produces serialization code at compile time: no reflection at run time, faster start, and trimming and Native AOT support. It is a Roslyn C# source generator, and the Visual Basic compiler in this SDK ran no generator for the Visual Basic project in the experiment below. A Visual Basic class that inherits JsonSerializerContext and carries <JsonSerializable> therefore stays abstract, and the build reports BC30610 for the members that the generator would have supplied.

Option Strict On
Imports System.Text.Json.Serialization

Public Class Book
    Public Property Title As String = ""
End Class

<JsonSerializable(GetType(Book))>
Partial Public Class ShelfJsonContext
    Inherits JsonSerializerContext
End Class

Module S04
    Sub Main()
    End Sub
End Module
S04.vb(9,22): error BC30610: Class 'ShelfJsonContext' must either be declared 'MustInherit' or override the following inherited 'MustOverride' member(s):

The workaround that keeps the application in Visual Basic is a small C# project that holds the context (and the generic converter), referenced by the Visual Basic project. The solution below has three projects: Models (Visual Basic, the data types), Json (C#, the generated context and the converter helper, referencing Models) and App (Visual Basic, referencing both). It was built and run as a whole on .NET 10 with TreatWarningsAsErrors in every project, and the Visual Basic project builds without warnings.

Models/Book.vb
Option Strict On

Public Class Book
    Public Property Isbn As Isbn
    Public Property Title As String = ""
    Public Property Year As Integer
End Class

Public Structure Isbn
    Public Sub New(value As String)
        Digits = value.Replace("-", "")
    End Sub

    Public ReadOnly Property Digits As String

    Public Overrides Function ToString() As String
        Return Digits
    End Function
End Structure
Json/Json.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <NuGetAudit>false</NuGetAudit>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Models\Models.vbproj" />
  </ItemGroup>
</Project>
Json/ShelfJsonContext.cs
using System.Text.Json.Serialization;
using Shelf.Models;

namespace Shelf.Json;

// The System.Text.Json source generator runs for C# projects only; this context is generated at compile time.
[JsonSourceGenerationOptions(WriteIndented = false, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Book))]
[JsonSerializable(typeof(List<Book>))]
public partial class ShelfJsonContext : JsonSerializerContext
{
}
Json/StringConverter.cs
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Shelf.Json;

// Visual Basic cannot name Utf8JsonReader, so it cannot override JsonConverter<T>.Read.
// This C# helper does the reading and writing and asks the caller for two functions.
public sealed class StringConverter<T> : JsonConverter<T>
{
    private readonly Func<T, string> _toText;
    private readonly Func<string, T> _fromText;

    public StringConverter(Func<T, string> toText, Func<string, T> fromText)
    {
        _toText = toText;
        _fromText = fromText;
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => _fromText(reader.GetString() ?? "");

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        => writer.WriteStringValue(_toText(value));
}
App/App.vbproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>Shelf.App</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Models\Models.vbproj" />
    <ProjectReference Include="..\Json\Json.csproj" />
  </ItemGroup>
</Project>
App/Program.vb
Option Strict On
Imports System.Text.Json
Imports Shelf.Json
Imports Shelf.Models

Module Program
    Sub Main()
        Dim book As New Book With {.Isbn = New Isbn("978-0-441-17271-9"), .Title = "Dune", .Year = 1965}

        ' Reflection-based serialization sees the Isbn structure as an object with a Digits property.
        Console.WriteLine(JsonSerializer.Serialize(book))

        ' A converter written in C# but configured from Visual Basic with two lambdas.
        Dim options As New JsonSerializerOptions(JsonSerializerDefaults.Web)
        options.Converters.Add(New StringConverter(Of Isbn)(Function(i) i.Digits, Function(s) New Isbn(s)))
        Dim text As String = JsonSerializer.Serialize(book, options)
        Console.WriteLine(text)
        Dim back As Book = JsonSerializer.Deserialize(Of Book)(text, options)
        Console.WriteLine($"Back: {back.Isbn} {back.Title} {back.Year}")

        ' The generated context, used from Visual Basic. The metadata comes from compile-time code, not from reflection.
        Dim ctx As ShelfJsonContext = ShelfJsonContext.Default
        Dim generated As String = JsonSerializer.Serialize(book, ctx.Book)
        Console.WriteLine(generated)
        Dim list As List(Of Book) = JsonSerializer.Deserialize(Of List(Of Book))("[{""title"":""Emma"",""year"":1815}]", ctx.ListBook)
        Console.WriteLine($"List via context: {list(0).Title} {list(0).Year}")
    End Sub
End Module
{"Isbn":{"Digits":"9780441172719"},"Title":"Dune","Year":1965}
{"isbn":"9780441172719","title":"Dune","year":1965}
Back: 9780441172719 Dune 1965
{"isbn":{"digits":"9780441172719"},"title":"Dune","year":1965}
List via context: Emma 1815

The four lines of output show the point: the reflection-based call writes the structure as an object; the converter (written in C#, configured from Visual Basic with two lambdas) writes it as a string and reads it back; and the generated context serializes through compile-time metadata (the property names are camel-cased by the generation options, and the Isbn structure is again an object because the context knows no converter for it). If the application is published trimmed, or compiled ahead of time, the reflection-based JsonSerializer.Serialize(value) overloads are the ones to avoid, since they depend on metadata that the trimmer may remove; pass the generated JsonTypeInfo as shown. Whether a Visual Basic application can be published trimmed or ahead of time at all depends on the project type and SDK; check the build output of your own project.

DOM, JsonDocument and Utf8JsonWriter

When the structure is not fixed, or only a few members matter, work with the document. JsonDocument is a read-only, pooled representation that must be disposed; JsonElement values obtained from it are valid only until then. JsonNode (JsonObject, JsonArray, JsonValue) is a mutable tree with an indexer, and Utf8JsonWriter writes tokens to a stream or buffer without building objects.

Option Strict On
Imports System.IO
Imports System.Text
Imports System.Text.Json
Imports System.Text.Json.Nodes

Module S03
    Sub Main()
        Dim json As String = "{""shelf"":""A"",""books"":[{""title"":""Dune"",""year"":1965,""tags"":[""sci-fi""]},{""title"":""Emma"",""year"":1815,""tags"":[]}]}"

        ' JsonDocument: read-only, pooled memory, must be disposed.
        Using doc As JsonDocument = JsonDocument.Parse(json)
            Dim root As JsonElement = doc.RootElement
            Console.WriteLine($"shelf = {root.GetProperty("shelf").GetString()}, books = {root.GetProperty("books").GetArrayLength()}")
            For Each book As JsonElement In root.GetProperty("books").EnumerateArray()
                Dim title As String = book.GetProperty("title").GetString()
                Dim year As Integer = book.GetProperty("year").GetInt32()
                Dim price As JsonElement = Nothing
                Console.WriteLine($"  {title} {year} tags={book.GetProperty("tags").GetArrayLength()} price present={book.TryGetProperty("price", price)}")
            Next
            Try
                root.GetProperty("missing")
            Catch ex As KeyNotFoundException
                Console.WriteLine("GetProperty on a missing name: KeyNotFoundException")
            End Try
            Try
                root.GetProperty("shelf").GetInt32()
            Catch ex As InvalidOperationException
                Console.WriteLine("GetInt32 on a string: InvalidOperationException")
            End Try
        End Using

        ' JsonNode: a mutable DOM.
        Dim node As JsonNode = JsonNode.Parse(json)
        node("shelf") = "B"
        Dim books As JsonArray = node("books").AsArray()
        books.Add(New JsonObject From {{"title", "Beloved"}, {"year", 1987}})
        books(1).AsObject().Remove("tags")
        Console.WriteLine(node.ToJsonString())
        Console.WriteLine($"Second title: {CStr(books(1)("title"))}, count {books.Count}, missing is Nothing: {books(0)("price") Is Nothing}")

        ' Utf8JsonWriter streams without building objects.
        Using stream As New MemoryStream()
            Using writer As New Utf8JsonWriter(stream, New JsonWriterOptions With {.Indented = True})
                writer.WriteStartObject()
                writer.WriteString("shelf", "C")
                writer.WriteStartArray("isbns")
                writer.WriteStringValue("978-0-441-17271-9")
                writer.WriteStringValue("978-0-14-143951-8")
                writer.WriteEndArray()
                writer.WriteNumber("count", 2)
                writer.WriteEndObject()
            End Using
            Console.WriteLine(Encoding.UTF8.GetString(stream.ToArray()))
        End Using
    End Sub
End Module
shelf = A, books = 2
  Dune 1965 tags=1 price present=False
  Emma 1815 tags=0 price present=False
GetProperty on a missing name: KeyNotFoundException
GetInt32 on a string: InvalidOperationException
{"shelf":"B","books":[{"title":"Dune","year":1965,"tags":["sci-fi"]},{"title":"Emma","year":1815},{"title":"Beloved","year":1987}]}
Second title: Emma, count 3, missing is Nothing: True
{
  "shelf": "C",
  "isbns": [
    "978-0-441-17271-9",
    "978-0-14-143951-8"
  ],
  "count": 2
}

GetProperty throws KeyNotFoundException for a missing member and TryGetProperty returns False; GetInt32 on a string throws InvalidOperationException; indexing a JsonNode with a missing name returns Nothing, so test before chaining. In Visual Basic a conversion from JsonNode to a value is explicit, CInt(node("year")) and CStr(node("title")), because the conversion operators are defined as narrowing in that direction.

Files, streams and async

JsonSerializer.SerializeAsync and DeserializeAsync work on any Stream, so a large collection streams to and from a file without building the whole text. DeserializeAsyncEnumerable yields the elements of a top-level array one by one. Visual Basic has no Await For Each, so the enumerator is driven by hand. JSON Lines (one compact object per line) is appendable and can be read with ReadAllLinesAsync or a StreamReader, which makes it a better log-like format than one large array.

Option Strict On
Imports System.IO
Imports System.Text
Imports System.Text.Json

Public Module S10
    Public Class Book
        Public Property Isbn As String = ""
        Public Property Title As String = ""
        Public Property Year As Integer
    End Class

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub

    Private Async Function RunAsync() As Task
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim books As New List(Of Book) From {
                New Book With {.Isbn = "978-0-441-17271-9", .Title = "Dune", .Year = 1965},
                New Book With {.Isbn = "978-0-14-143951-8", .Title = "Emma", .Year = 1815},
                New Book With {.Isbn = "978-0-14-243724-7", .Title = "Beloved", .Year = 1987}}
            Dim options As New JsonSerializerOptions With {.WriteIndented = True}

            ' Save: stream straight to a file, without building the whole text in memory.
            Dim target As String = Path.Combine(root, "catalog.json")
            Using stream As New FileStream(target, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync:=True)
                Await JsonSerializer.SerializeAsync(stream, books, options)
            End Using
            Console.WriteLine($"Wrote {New FileInfo(target).Length} bytes, first line: {File.ReadLines(target).First()}")

            ' Load the whole list.
            Dim loaded As List(Of Book)
            Using stream As New FileStream(target, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync:=True)
                loaded = Await JsonSerializer.DeserializeAsync(Of List(Of Book))(stream)
            End Using
            Console.WriteLine($"Loaded {loaded.Count} books; the last is {loaded(loaded.Count - 1).Title}")

            ' Or consume the array element by element (VB has no Await For Each, so drive the enumerator by hand).
            Dim titles As New List(Of String)()
            Using stream As New FileStream(target, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync:=True)
                Dim enumerator As IAsyncEnumerator(Of Book) = JsonSerializer.DeserializeAsyncEnumerable(Of Book)(stream).GetAsyncEnumerator()
                Do While Await enumerator.MoveNextAsync()
                    titles.Add(enumerator.Current.Title)
                Loop
                Await enumerator.DisposeAsync()
            End Using
            Console.WriteLine("Streamed titles: " & String.Join(", ", titles))

            ' JSON Lines: one compact object per line, appendable and splittable.
            Dim lines As String = Path.Combine(root, "catalog.jsonl")
            Dim written As New List(Of String)()
            For Each b As Book In books
                written.Add(JsonSerializer.Serialize(b))
            Next
            Await File.WriteAllLinesAsync(lines, written)
            File.AppendAllText(lines, JsonSerializer.Serialize(New Book With {.Isbn = "x", .Title = "Appended", .Year = 2026}) & Environment.NewLine)
            Dim count As Integer = 0
            For Each line As String In Await File.ReadAllLinesAsync(lines)
                Dim b As Book = JsonSerializer.Deserialize(Of Book)(line)
                count += 1
                If count = 4 Then Console.WriteLine($"Fourth line: {b.Title}")
            Next
            Console.WriteLine($"JSON Lines count: {count}")

            ' A truncated file is a JsonException, not a partial list.
            File.WriteAllText(target, File.ReadAllText(target).Substring(0, 60))
            Try
                Using stream As New FileStream(target, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync:=True)
                    loaded = Await JsonSerializer.DeserializeAsync(Of List(Of Book))(stream)
                End Using
            Catch ex As JsonException
                Console.WriteLine($"Truncated file: {ex.GetType().Name} at line {ex.LineNumber}")
            End Try
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Function
End Module
Wrote 245 bytes, first line: [
Loaded 3 books; the last is Beloved
Streamed titles: Dune, Emma, Beloved
Fourth line: Appended
JSON Lines count: 4
Truncated file: JsonException at line 4

A truncated file is a JsonException, not a partial list: the deserializer cannot return a half-read array. Writing straight to the final file has the same risk in the other direction, so replace files atomically as described on the files page.

Polymorphism, unknown members, depth and numbers

By default the serializer writes only the declared type of a property. For a class hierarchy, <JsonPolymorphic> and <JsonDerivedType> opt in to a type discriminator and, importantly, close the set of types: only the registered derived types can be written or read, and an unknown discriminator is rejected. That is the safe design, unlike serializers that read an arbitrary type name from the input.

Option Strict On
Imports System.Text
Imports System.Text.Json
Imports System.Text.Json.Serialization

Public Module S09
    <JsonPolymorphic(TypeDiscriminatorPropertyName:="kind")>
    <JsonDerivedType(GetType(Film), "film")>
    <JsonDerivedType(GetType(Magazine), "magazine")>
    Public MustInherit Class Media
        Public Property Title As String = ""
    End Class

    Public Class Film
        Inherits Media
        Public Property Minutes As Integer
    End Class

    Public Class Magazine
        Inherits Media
        Public Property Issue As Integer
    End Class

    Public Class Evil
        Inherits Media
    End Class

    <JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)>
    Public Class StrictBook
        Public Property Title As String = ""
    End Class

    Public Class LooseBook
        Public Property Title As String = ""
    End Class

    Sub Main()
        ' Polymorphism is opt-in and closed: only the listed derived types can be written or read.
        Dim items As New List(Of Media) From {New Film With {.Title = "Dune", .Minutes = 155}, New Magazine With {.Title = "Wired", .Issue = 7}}
        Dim json As String = JsonSerializer.Serialize(items)
        Console.WriteLine(json)
        Dim back As List(Of Media) = JsonSerializer.Deserialize(Of List(Of Media))(json)
        Console.WriteLine(String.Join(", ", back.Select(Function(m) m.GetType().Name & ":" & m.Title)))

        Try
            JsonSerializer.Deserialize(Of Media)("{""kind"":""System.Diagnostics.Process"",""Title"":""x""}")
        Catch ex As JsonException
            Console.WriteLine($"Unknown discriminator: {ex.GetType().Name}: {ex.Message}")
        End Try
        Try
            JsonSerializer.Serialize(Of Media)(New Evil())
        Catch ex As NotSupportedException
            Console.WriteLine($"Unregistered subtype: {ex.GetType().Name}: {ex.Message}")
        End Try
        Try
            JsonSerializer.Deserialize(Of Media)("{""Title"":""no discriminator""}")
        Catch ex As Exception When TypeOf ex Is NotSupportedException OrElse TypeOf ex Is JsonException
            Console.WriteLine($"Missing discriminator on an abstract type: {ex.GetType().Name}: {ex.Message}")
        End Try

        ' Unknown members: ignored by default, an error on request.
        Dim extra As String = "{""Title"":""Dune"",""Surprise"":1}"
        Console.WriteLine($"Loose: {JsonSerializer.Deserialize(Of LooseBook)(extra).Title}")
        Try
            JsonSerializer.Deserialize(Of StrictBook)(extra)
        Catch ex As JsonException
            Console.WriteLine($"Strict: {ex.Message}")
        End Try
        Dim strictOptions As New JsonSerializerOptions With {.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow}
        Try
            JsonSerializer.Deserialize(Of LooseBook)(extra, strictOptions)
        Catch ex As JsonException
            Console.WriteLine("Options-wide Disallow: JsonException")
        End Try

        ' Depth limit: 64 levels by default, so hostile nesting fails fast instead of exhausting the stack.
        Dim deep As String = New String("["c, 100) & New String("]"c, 100)
        Try
            JsonSerializer.Deserialize(Of JsonElement)(deep)
        Catch ex As JsonException
            Console.WriteLine($"Depth 100 with default MaxDepth: {ex.Message.Substring(0, 56)}")
        End Try
        Dim deeper As New JsonSerializerOptions With {.MaxDepth = 200}
        Console.WriteLine($"Depth 100 with MaxDepth=200: {JsonSerializer.Deserialize(Of JsonElement)(deep, deeper).ValueKind}")

        ' Numbers: strict by default.
        Try
            JsonSerializer.Deserialize(Of Integer)("""42""")
        Catch ex As JsonException
            Console.WriteLine("A string is not accepted where a number is expected: JsonException")
        End Try
        Dim numbers As New JsonSerializerOptions With {.NumberHandling = JsonNumberHandling.AllowReadingFromString}
        Console.WriteLine($"AllowReadingFromString: {JsonSerializer.Deserialize(Of Integer)("""42""", numbers)}")
        Try
            JsonSerializer.Deserialize(Of Byte)("300")
        Catch ex As JsonException
            Console.WriteLine("300 as Byte: JsonException (no silent wrap-around)")
        End Try
    End Sub
End Module
[{"kind":"film","Minutes":155,"Title":"Dune"},{"kind":"magazine","Issue":7,"Title":"Wired"}]
Film:Dune, Magazine:Wired
Unknown discriminator: JsonException: Read unrecognized type discriminator id 'System.Diagnostics.Process'. Path: $ | LineNumber: 0 | BytePositionInLine: 45.
Unregistered subtype: NotSupportedException: Runtime type 'ShelfDocs.S09+Evil' is not supported by polymorphic type 'ShelfDocs.S09+Media'. Path: $.
Missing discriminator on an abstract type: NotSupportedException: The JSON payload for polymorphic interface or abstract type 'ShelfDocs.S09+Media' must specify a type discriminator. Path: $ | LineNumber: 0 | BytePositionInLine: 9.
Loose: Dune
Strict: The JSON property 'Surprise' could not be mapped to any .NET member contained in type 'ShelfDocs.S09+StrictBook'.
Options-wide Disallow: JsonException
Depth 100 with default MaxDepth: The maximum configured depth of 64 has been exceeded. Ca
Depth 100 with MaxDepth=200: Array
A string is not accepted where a number is expected: JsonException
AllowReadingFromString: 42
300 as Byte: JsonException (no silent wrap-around)

The exception types differ: an unrecognised discriminator in the input is a JsonException, a runtime type that is not registered, or a missing discriminator on an abstract base, is NotSupportedException. Unknown members are skipped by default and rejected with UnmappedMemberHandling.Disallow (on the type or on the options). The default depth limit of 64 turns hostile nesting into a JsonException instead of a stack overflow; raise it only for known-deep data. Numbers are strict: "42" is not an integer unless AllowReadingFromString is set, and 300 does not wrap into a Byte.

XmlSerializer

XmlSerializer maps public read/write properties and public fields of public classes to XML, using attributes to choose element or attribute names, namespaces and order. The class needs a public parameterless constructor, and the type itself must be public. It is the serializer for XML that has a defined schema or an existing consumer.

Option Strict On
Imports System.IO
Imports System.Xml
Imports System.Xml.Serialization

Public Module S05
    Public Enum LoanState
        Available
        OnLoan
    End Enum

    <XmlRoot("book", Namespace:="urn:shelf")>
    Public Class Book
        <XmlAttribute("isbn")>
        Public Property Isbn As String = ""

        <XmlElement("title")>
        Public Property Title As String = ""

        <XmlElement("author")>
        Public Property Authors As New List(Of String)()

        <XmlArray("tags")>
        <XmlArrayItem("tag")>
        Public Property Tags As New List(Of String)()

        <XmlElement("year")>
        Public Property Year As Integer

        <XmlIgnore>
        Public Property InternalNote As String = ""

        <XmlElement("state")>
        Public Property State As LoanState

        <XmlElement("due", DataType:="date")>
        Public Property DueOn As Date

        <XmlElement("price")>
        Public Property Price As Decimal?
    End Class

    Function Serialize(Of T)(value As T, Optional omitDeclaration As Boolean = False) As String
        Dim serializer As New XmlSerializer(GetType(T))
        Dim ns As New XmlSerializerNamespaces()
        ns.Add("", "urn:shelf")       ' suppress xmlns:xsi and xmlns:xsd, keep the default namespace
        Using sw As New StringWriter()
            Using xw As XmlWriter = XmlWriter.Create(sw, New XmlWriterSettings With {.Indent = True, .OmitXmlDeclaration = omitDeclaration})
                serializer.Serialize(xw, value, ns)
            End Using
            Return sw.ToString()
        End Using
    End Function

    Sub Main()
        Dim dune As New Book With {.Isbn = "978-0-441-17271-9", .Title = "Dune", .Year = 1965, .State = LoanState.OnLoan,
                                   .DueOn = New Date(2026, 2, 3, 0, 0, 0), .InternalNote = "hidden"}
        dune.Authors.Add("Frank Herbert")
        dune.Tags.Add("sci-fi")
        dune.Tags.Add("classic")

        Dim xml As String = Serialize(dune, omitDeclaration:=True)
        Console.WriteLine(xml)

        Dim serializer As New XmlSerializer(GetType(Book))
        Using reader As New StringReader(xml)
            Dim back As Book = DirectCast(serializer.Deserialize(reader), Book)
            Console.WriteLine($"Back: {back.Isbn} {back.Title} {back.Year} {back.State} {back.DueOn:yyyy-MM-dd} tags={back.Tags.Count} price has value={back.Price.HasValue} note=[{back.InternalNote}]")
        End Using

        ' Default naming: without attributes the element names are the property names.
        Dim plain As New XmlSerializer(GetType(LoanRecord))
        Using sw As New StringWriter()
            plain.Serialize(sw, New LoanRecord With {.Member = "ada", .Renewals = 1})
            Console.WriteLine(sw.ToString().Replace(vbCrLf, vbLf))
        End Using

        ' Deserializing wrong XML: the exception message names the line and position.
        Try
            Using reader As New StringReader("<book xmlns=""urn:shelf""><year>nineteen</year></book>")
                serializer.Deserialize(reader)
            End Using
        Catch ex As InvalidOperationException
            Console.WriteLine($"InvalidOperationException: {ex.Message}")
            Console.WriteLine($"Inner: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}")
        End Try
        Try
            Using reader As New StringReader("<other xmlns=""urn:shelf""/>")
                serializer.Deserialize(reader)
            End Using
        Catch ex As InvalidOperationException
            Console.WriteLine($"Wrong root element: {ex.Message} {ex.InnerException.Message}")
        End Try
    End Sub

    Public Class LoanRecord
        Public Property Member As String = ""
        Public Property Renewals As Integer
    End Class
End Module
<book isbn="978-0-441-17271-9" xmlns="urn:shelf">
  <title>Dune</title>
  <author>Frank Herbert</author>
  <tags>
    <tag>sci-fi</tag>
    <tag>classic</tag>
  </tags>
  <year>1965</year>
  <state>OnLoan</state>
  <due>2026-02-03</due>
  <price p2:nil="true" xmlns:p2="http://www.w3.org/2001/XMLSchema-instance" />
</book>
Back: 978-0-441-17271-9 Dune 1965 OnLoan 2026-02-03 tags=2 price has value=False note=[]
<?xml version="1.0" encoding="utf-16"?>
<LoanRecord xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Member>ada</Member>
  <Renewals>1</Renewals>
</LoanRecord>
InvalidOperationException: There is an error in XML document (1, 48).
Inner: FormatException: The input string 'nineteen' was not in a correct format.
Wrong root element: There is an error in XML document (1, 2). <other xmlns='urn:shelf'> was not expected.
Attribute Effect

<XmlRoot("name", Namespace:=...)>

Name and namespace of the root element.

<XmlElement("name")>

Name of the element for a property; on a collection it removes the wrapper element, so each item becomes a sibling.

<XmlAttribute("name")>

Write the property as an attribute.

<XmlArray("x")>, <XmlArrayItem("y")>

Wrapper element and item name for a collection.

<XmlIgnore>

Skip the member.

<XmlText>

The property is the element’s text content.

<XmlInclude(GetType(Derived))>

Declare a derived type that may appear where the base type is declared.

<XmlEnum("name")>

Text of an enum member.

Details in the output: the default xmlns:xsi and xmlns:xsd declarations are suppressed by passing an XmlSerializerNamespaces instance with only the namespace you want; a Decimal? that is Nothing becomes xsi:nil="true"; DataType:="date" writes only the date; OmitXmlDeclaration on the XmlWriterSettings removes the xml declaration, which otherwise says utf-16 when written to a StringWriter. The exception for malformed input is always InvalidOperationException, with the line and position in the message and the real cause in InnerException. Without any attributes, the element names are the property names, as the LoanRecord shows.

The second program lists the rules that surprise people, each run for real.

Option Strict On
Imports System.IO
Imports System.Xml.Serialization

Public Module S05b
    Public Class WithDictionary
        Public Property Prices As New Dictionary(Of String, Decimal)()
    End Class

    Public Class NoDefaultConstructor
        Public Sub New(title As String)
            Me.Title = title
        End Sub
        Public Property Title As String
    End Class

    Public Class WithPrivateSet
        Public Property Title As String = ""
        Public ReadOnly Property Computed As String
            Get
                Return "ignored"
            End Get
        End Property
        Public Property Locked As String = "x"
    End Class

    Public Interface IShape
    End Interface

    Public Class Holder
        Public Property Shape As IShape
    End Class

    Public MustInherit Class Media
        Public Property Title As String = ""
    End Class

    Public Class Film
        Inherits Media
        Public Property Minutes As Integer
    End Class

    Public Class Shelf
        Public Property Items As New List(Of Media)()
    End Class

    Private Sub Attempt(label As String, action As Action)
        Try
            action()
            Console.WriteLine(label & ": works")
        Catch ex As Exception
            Dim inner As Exception = If(ex.InnerException, ex)
            Console.WriteLine($"{label}: {ex.GetType().Name} -> {If(inner.Message.Length > 100, inner.Message.Substring(0, 100) & "...", inner.Message)}")
        End Try
    End Sub

    Private Function Create(type As Type) As XmlSerializer
        Return New XmlSerializer(type)
    End Function

    Private Function ToXml(type As Type, value As Object) As String
        Using sw As New StringWriter()
            Dim serializer As New XmlSerializer(type)
            serializer.Serialize(sw, value)
            Return sw.ToString()
        End Using
    End Function

    Sub Main()
        Attempt("Dictionary member", Sub() ToXml(GetType(WithDictionary), New WithDictionary()))
        Attempt("No parameterless constructor", Sub() Create(GetType(NoDefaultConstructor)))
        Attempt("Interface member", Sub() ToXml(GetType(Holder), New Holder()))

        Dim shelf As New Shelf()
        shelf.Items.Add(New Film With {.Title = "Dune", .Minutes = 155})
        Attempt("Derived type not declared", Sub() ToXml(GetType(Shelf), shelf))

        Dim overrideSet As New XmlAttributeOverrides()
        Dim attrs As New XmlAttributes()
        attrs.XmlElements.Add(New XmlElementAttribute("film", GetType(Film)))
        overrideSet.Add(GetType(Shelf), "Items", attrs)
        Using sw As New StringWriter()
            Dim serializer As New XmlSerializer(GetType(Shelf), overrideSet)
            serializer.Serialize(sw, shelf)
            Console.WriteLine("With XmlAttributeOverrides: " & sw.ToString().Contains("<film>"))
        End Using

        Console.WriteLine("Read-only property serialized: " & ToXml(GetType(WithPrivateSet), New WithPrivateSet()).Contains("Computed"))
    End Sub
End Module
Dictionary member: InvalidOperationException -> Cannot serialize member ShelfDocs.S05b+WithDictionary.Prices of type System.Collections.Generic.Dict...
No parameterless constructor: InvalidOperationException -> ShelfDocs.S05b.NoDefaultConstructor cannot be serialized because it does not have a parameterless co...
Interface member: InvalidOperationException -> Cannot serialize member 'ShelfDocs.S05b+Holder.Shape' of type 'ShelfDocs.S05b+IShape', see inner exc...
Derived type not declared: InvalidOperationException -> The type ShelfDocs.S05b+Film was not expected. Use the XmlInclude or SoapInclude attribute to specif...
With XmlAttributeOverrides: True
Read-only property serialized: False

An XmlSerializer is also created by reflection and generating code at run time, which is slow: create one serializer per type and reuse it. The serializers created with the constructor that takes only a Type are cached by the runtime; the ones created with overrides or extra types are not, so keep those in a field.

A Visual Basic detail that costs hours: a Module is Friend by default, so every class declared inside one is not public, and XmlSerializer refuses it with a message that does not mention the module. System.Text.Json reads non-public types, so the same class serializes to JSON without complaint.

Option Strict On
Imports System.IO
Imports System.Text.Json
Imports System.Xml.Serialization

' A Module is Friend by default, so Book, declared inside it, is not a public type.
Module S05c
    Public Class Book
        Public Property Title As String = "Dune"
    End Class

    Sub Main()
        ' System.Text.Json reads non-public types through reflection, so this works.
        Console.WriteLine(JsonSerializer.Serialize(New Book()))
        ' XmlSerializer generates code that must be able to name the type, so it insists on public.
        Try
            Dim serializer As New XmlSerializer(GetType(Book))
            serializer.Serialize(New StringWriter(), New Book())
        Catch ex As InvalidOperationException
            Console.WriteLine(ex.Message)
        End Try
    End Sub
End Module
{"Title":"Dune"}
ShelfDocs.S05c+Book is inaccessible due to its protection level. Only public types can be processed.

The fix is Public Module, or, better, putting the data classes in their own files outside any module. The examples on this page that use XmlSerializer and DataContractSerializer declare Public Module for that reason.

DataContractSerializer

DataContractSerializer comes from WCF. It is opt-in: only members marked <DataMember> in a class marked <DataContract> are serialized, whatever their access modifier, and no parameterless constructor is needed (the constructor does not run during deserialization). It writes XML with namespaces, and DataContractJsonSerializer writes the same contract as JSON. The element order is part of the contract: alphabetical by default, or by Order.

Option Strict On
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Json
Imports System.Text
Imports System.Xml

Public Module S06
    <DataContract(Name:="Book", Namespace:="urn:shelf")>
    Public Class Book
        <DataMember(Name:="isbn", Order:=1, IsRequired:=True)>
        Public Property Isbn As String

        <DataMember(Name:="title", Order:=2)>
        Public Property Title As String

        <DataMember(Name:="year", Order:=3)>
        Public Property Year As Integer

        <DataMember(Name:="note", Order:=4, EmitDefaultValue:=False)>
        Public Property Note As String

        ' A private field can be a data member: no public setter is needed.
        <DataMember(Name:="shelfMark", Order:=5)>
        Private _shelfMark As String = "A-12"

        ' Not marked: not serialized.
        Public Property Cache As String = "ignored"

        Public Sub New(isbn As String, title As String, year As Integer)
            Me.Isbn = isbn
            Me.Title = title
            Me.Year = year
        End Sub

        Public ReadOnly Property ShelfMark As String
            Get
                Return _shelfMark
            End Get
        End Property

        <OnDeserializing>
        Private Sub OnDeserializing(context As StreamingContext)
            ' Constructors do not run during deserialization, so field initializers are not applied either.
            _shelfMark = "(not set by constructor)"
        End Sub
    End Class

    <DataContract>
    Public Enum LoanState
        <EnumMember(Value:="available")> Available
        <EnumMember(Value:="on-loan")> OnLoan
    End Enum

    <DataContract(Name:="Loan", Namespace:="urn:shelf")>
    Public Class Loan
        <DataMember> Public Property State As LoanState
        <DataMember> Public Property DueOn As Date
    End Class

    Function ToXml(Of T)(value As T) As String
        Dim serializer As New DataContractSerializer(GetType(T))
        Using stream As New MemoryStream()
            Using writer As XmlWriter = XmlWriter.Create(stream, New XmlWriterSettings With {.Indent = True, .OmitXmlDeclaration = True, .Encoding = New UTF8Encoding(False)})
                serializer.WriteObject(writer, value)
            End Using
            Return Encoding.UTF8.GetString(stream.ToArray())
        End Using
    End Function

    Sub Main()
        Dim dune As New Book("978-0-441-17271-9", "Dune", 1965)
        Dim xml As String = ToXml(dune)
        Console.WriteLine(xml)

        Dim serializer As New DataContractSerializer(GetType(Book))
        Using reader As XmlReader = XmlReader.Create(New StringReader(xml))
            Dim back As Book = DirectCast(serializer.ReadObject(reader), Book)
            Console.WriteLine($"Back: {back.Isbn} {back.Title} {back.Year} note=[{back.Note}] shelfMark={back.ShelfMark} cache={back.Cache Is Nothing}")
        End Using

        Console.WriteLine(ToXml(New Loan With {.State = LoanState.OnLoan, .DueOn = New Date(2026, 2, 3, 0, 0, 0)}))

        ' The same contract as JSON.
        Dim json As New DataContractJsonSerializer(GetType(Book))
        Using stream As New MemoryStream()
            json.WriteObject(stream, dune)
            Console.WriteLine(Encoding.UTF8.GetString(stream.ToArray()))
        End Using

        ' Order matters: members must appear in the declared order. A misplaced required member is an error...
        Dim wrongOrder As String = "<Book xmlns=""urn:shelf""><title>Dune</title><isbn>1</isbn></Book>"
        Try
            Using reader As XmlReader = XmlReader.Create(New StringReader(wrongOrder))
                serializer.ReadObject(reader)
            End Using
        Catch ex As SerializationException
            Console.WriteLine($"SerializationException: {ex.Message}")
        End Try

        ' ...and a misplaced optional member is silently skipped.
        Dim skipped As String = "<Book xmlns=""urn:shelf""><isbn>1</isbn><year>1965</year><title>Dune</title></Book>"
        Using reader As XmlReader = XmlReader.Create(New StringReader(skipped))
            Dim b As Book = DirectCast(serializer.ReadObject(reader), Book)
            Console.WriteLine($"Out-of-order optional member: year={b.Year} title=[{b.Title}]")
        End Using

        ' The attribute-free alternative: plain classes serialize all public members with their own names.
        Console.WriteLine(ToXml(New Plain With {.Name = "Ada"}).Replace(vbCrLf, vbLf))
    End Sub

    Public Class Plain
        Public Property Name As String
        Private Secret As String = "kept private"
    End Class
End Module
<Book xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:shelf">
  <isbn>978-0-441-17271-9</isbn>
  <title>Dune</title>
  <year>1965</year>
  <shelfMark>A-12</shelfMark>
</Book>
Back: 978-0-441-17271-9 Dune 1965 note=[] shelfMark=A-12 cache=True
<Loan xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:shelf">
  <DueOn>2026-02-03T00:00:00</DueOn>
  <State>on-loan</State>
</Loan>
{"isbn":"978-0-441-17271-9","title":"Dune","year":1965,"shelfMark":"A-12"}
SerializationException: Error in line 1 position 26. 'Element' 'title' from namespace 'urn:shelf' is not expected. Expecting element 'isbn'.
Out-of-order optional member: year=1965 title=[]
<S06.Plain xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ShelfDocs">
  <Name>Ada</Name>
</S06.Plain>

Observe: EmitDefaultValue:=False omitted the empty Note; the private field _shelfMark was written and read through <DataMember>; the constructor did not run on deserialization, so a field initializer would not have been applied (the <OnDeserializing> callback is where such defaults are set); <EnumMember(Value:="on-loan")> renamed the enum text; a member out of order is an error if required and silently dropped if optional (the second kind of failure is the dangerous one, because nothing is reported); and a class without <DataContract> falls back to serializing its public members, with a name and namespace derived from the CLR type, which for a class nested in a module includes the module name (S06.Plain).

Versioning data contracts

Data that outlives a program version meets older and newer readers. A reader that drops what it does not understand and writes the document back deletes the newer data. IExtensibleDataObject is the round-trip fix: the old class implements it, the unknown content is stored in ExtensionData, and it is written back unchanged. XmlSerializer takes the opposite approach and reports unknown content through events, so the program decides.

Option Strict On
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Xml
Imports System.Xml.Serialization

Public Module S09b
    ' Version 1 of the contract. IExtensibleDataObject lets it carry members it does not know yet.
    <DataContract(Name:="Book", Namespace:="urn:shelf")>
    Public Class BookV1
        Implements IExtensibleDataObject

        <DataMember(Order:=1)> Public Property Title As String
        <DataMember(Order:=2)> Public Property Year As Integer

        Public Property ExtensionData As ExtensionDataObject Implements IExtensibleDataObject.ExtensionData
    End Class

    <DataContract(Name:="Book", Namespace:="urn:shelf")>
    Public Class BookV1Plain
        <DataMember(Order:=1)> Public Property Title As String
        <DataMember(Order:=2)> Public Property Year As Integer
    End Class

    <XmlRoot("book")>
    Public Class XmlBook
        Public Property Title As String
    End Class

    Const V2Xml As String = "<Book xmlns=""urn:shelf"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""><Title>Dune</Title><Year>1965</Year><Price>9.99</Price></Book>"

    Function RoundTrip(Of T)(xml As String) As String
        Dim serializer As New DataContractSerializer(GetType(T))
        Dim value As Object
        Using reader As XmlReader = XmlReader.Create(New StringReader(xml))
            value = serializer.ReadObject(reader)
        End Using
        Using sw As New StringWriter()
            Using writer As XmlWriter = XmlWriter.Create(sw, New XmlWriterSettings With {.OmitXmlDeclaration = True})
                serializer.WriteObject(writer, value)
            End Using
            Return sw.ToString()
        End Using
    End Function

    Sub Main()
        ' Version 2 data (it has a Price) goes through an old program that does not know Price.
        Console.WriteLine("Without IExtensibleDataObject: " & RoundTrip(Of BookV1Plain)(V2Xml))
        Console.WriteLine("With IExtensibleDataObject:    " & RoundTrip(Of BookV1)(V2Xml))

        ' Unknown content can also be refused outright.
        Dim strictSerializer As New DataContractSerializer(GetType(BookV1Plain))
        Console.WriteLine($"IgnoreExtensionDataObject default: {strictSerializer.IgnoreExtensionDataObject}")

        ' XmlSerializer reports what it does not recognise through events, instead of failing.
        Dim serializer As New XmlSerializer(GetType(XmlBook))
        AddHandler serializer.UnknownElement, Sub(sender As Object, e As XmlElementEventArgs)
                                                  Console.WriteLine($"Unknown element <{e.Element.Name}> at line {e.LineNumber}")
                                              End Sub
        AddHandler serializer.UnknownAttribute, Sub(sender As Object, e As XmlAttributeEventArgs)
                                                    Console.WriteLine($"Unknown attribute {e.Attr.Name}")
                                                End Sub
        Using reader As New StringReader("<book flag=""1""><Title>Dune</Title><Price>9.99</Price></book>")
            Dim book As XmlBook = DirectCast(serializer.Deserialize(reader), XmlBook)
            Console.WriteLine($"Title still read: {book.Title}")
        End Using
    End Sub
End Module
Without IExtensibleDataObject: <Book xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:shelf"><Title>Dune</Title><Year>1965</Year></Book>
With IExtensibleDataObject:    <Book xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:shelf"><Title>Dune</Title><Year>1965</Year><Price>9.99</Price></Book>
IgnoreExtensionDataObject default: False
Unknown attribute flag
Unknown element <Price> at line 1
Title still read: Dune

The rules for compatible changes are the same for every serializer: add optional members and never reuse a name for a different meaning; do not rename without an explicit name attribute; do not change a member’s type; prefer an explicit version field in the data when changes are not additive; keep old files around as test fixtures and deserialize them in tests.

XmlReader, XmlWriter, XmlDocument and XPath

When no object mapping is wanted, work with the XML. XmlReader is a forward-only pull parser with constant memory; XmlWriter writes well-formed XML and escapes text for you; XmlDocument is the older mutable DOM; XPathNavigator and XPath queries select nodes; XDocument (LINQ to XML) is the modern API and the one that XML literals build. The namespace is a constant source of mistakes: XPath has no default namespace, so a document with xmlns="urn:shelf" needs an XmlNamespaceManager with a prefix of your choosing, and the query without the prefix finds nothing.

Option Strict On
Imports System.IO
Imports System.Text
Imports System.Xml
Imports System.Xml.XPath

Public Module S07
    Const Catalog As String =
        "<?xml version=""1.0"" encoding=""utf-8""?>" & vbLf &
        "<catalog xmlns=""urn:shelf"" updated=""2026-01-15"">" & vbLf &
        "  <book isbn=""978-0-441-17271-9""><title>Dune</title><year>1965</year><price>9.99</price></book>" & vbLf &
        "  <book isbn=""978-0-14-143951-8""><title>Emma</title><year>1815</year><price>7.50</price></book>" & vbLf &
        "  <book isbn=""978-0-14-243724-7""><title>Beloved</title><year>1987</year></book>" & vbLf &
        "</catalog>"

    Sub Main()
        ' XmlReader: forward-only, low memory.
        Dim settings As New XmlReaderSettings With {.IgnoreWhitespace = True, .IgnoreComments = True}
        Using reader As XmlReader = XmlReader.Create(New StringReader(Catalog), settings)
            Do While reader.Read()
                If reader.NodeType = XmlNodeType.Element AndAlso reader.LocalName = "book" Then
                    Dim isbn As String = reader.GetAttribute("isbn")
                    reader.ReadToDescendant("title", "urn:shelf")
                    Dim title As String = reader.ReadElementContentAsString()
                    Console.WriteLine($"{isbn} {title}")
                End If
            Loop
        End Using

        ' A cleaner loop: ReadToFollowing and ReadSubtree.
        Using reader As XmlReader = XmlReader.Create(New StringReader(Catalog), settings)
            Dim years As New List(Of Integer)()
            Do While reader.ReadToFollowing("year", "urn:shelf")
                years.Add(reader.ReadElementContentAsInt())
            Loop
            Console.WriteLine($"Years: {String.Join(", ", years)}")
        End Using

        ' XmlWriter: well-formed output without building a tree.
        Dim sb As New StringBuilder()
        Using writer As XmlWriter = XmlWriter.Create(sb, New XmlWriterSettings With {.Indent = True, .OmitXmlDeclaration = True})
            writer.WriteStartElement("loans", "urn:shelf")
            writer.WriteAttributeString("count", "2")
            writer.WriteStartElement("loan")
            writer.WriteElementString("member", "Ann & Bob <team>")   ' escaped automatically
            writer.WriteEndElement()
            writer.WriteComment(" more loans follow ")
            writer.WriteEndElement()
        End Using
        Console.WriteLine(sb.ToString().Replace(vbCrLf, vbLf))

        ' XmlDocument: a mutable DOM and XPath with a namespace manager.
        Dim doc As New XmlDocument()
        doc.LoadXml(Catalog)
        Dim ns As New XmlNamespaceManager(doc.NameTable)
        ns.AddNamespace("s", "urn:shelf")
        Dim titles As XmlNodeList = doc.SelectNodes("/s:catalog/s:book[s:year < 1900]/s:title", ns)
        Console.WriteLine($"Titles before 1900: {String.Join(", ", titles.Cast(Of XmlNode)().Select(Function(n) n.InnerText))}")
        Console.WriteLine($"Without the prefix nothing matches: {doc.SelectNodes("/catalog/book").Count}")
        Dim firstBook As XmlElement = DirectCast(doc.SelectSingleNode("//s:book[@isbn='978-0-14-143951-8']", ns), XmlElement)
        firstBook.SetAttribute("shelf", "B")
        Dim price As XmlNode = doc.SelectSingleNode("//s:book[s:title='Beloved']/s:price", ns)
        Console.WriteLine($"Missing node is Nothing: {price Is Nothing}")
        Console.WriteLine($"Edited element: {firstBook.OuterXml.Substring(0, 62)}")

        ' XPathNavigator: read-only cursor, and XPath functions.
        Dim nav As XPathNavigator = doc.CreateNavigator()
        Dim total As Object = nav.Evaluate("sum(//s:book/s:price)", ns)
        Console.WriteLine($"Sum of prices: {total}")
        Dim expr As XPathExpression = nav.Compile("count(//s:book)")
        expr.SetContext(ns)
        Console.WriteLine($"Books: {nav.Evaluate(expr)}")

        ' Invalid XML.
        Try
            Dim bad As New XmlDocument()
            bad.LoadXml("<a><b></a>")
        Catch ex As XmlException
            Console.WriteLine($"XmlException line {ex.LineNumber}, position {ex.LinePosition}")
        End Try
        Try
            Dim bad As New XmlDocument()
            bad.LoadXml("<a>" & ChrW(&H1) & "</a>")
        Catch ex As XmlException
            Console.WriteLine("Control character: XmlException")
        End Try
    End Sub
End Module
978-0-441-17271-9 Dune
978-0-14-143951-8 Emma
978-0-14-243724-7 Beloved
Years: 1965, 1815, 1987
<loans count="2" xmlns="urn:shelf">
  <loan>
    <member>Ann &amp; Bob &lt;team&gt;</member>
  </loan>
  <!-- more loans follow -->
</loans>
Titles before 1900: Emma
Without the prefix nothing matches: 0
Missing node is Nothing: True
Edited element: <book isbn="978-0-14-143951-8" shelf="B" xmlns="urn:shelf"><ti
Sum of prices: 17.490000000000002
Books: 3
XmlException line 1, position 9
Control character: XmlException

Notes from the run: ReadElementContentAsString cannot be called on an element with children (it threw during development of this example), so the loop moves to the child first; XmlWriter escaped & and the angle brackets; XPathNavigator.Evaluate computes with Double, which is why the sum prints as 17.490000000000002, a floating-point artefact that a Decimal calculation in Visual Basic would not produce; and XmlException carries LineNumber and LinePosition.

XML security: DTDs, entities and resolvers

XML can define entities, and an external entity can point to a file or a URL. A parser that resolves external entities from untrusted input lets an attacker read local files or reach internal servers (XXE), and a document whose entities expand into each other can consume memory (the "billion laughs" attack). The defaults of current .NET are safe, and the rule is to keep them: XmlReader prohibits DTDs unless you allow them, XmlDocument.LoadXml uses no resolver, and XmlReaderSettings.XmlResolver is Nothing.

Option Strict On
Imports System.IO
Imports System.Xml

Public Module S07b
    Const WithDtd As String =
        "<?xml version=""1.0""?>" & vbLf &
        "<!DOCTYPE catalog [ <!ENTITY owner ""Shelf Library""> <!ENTITY big SYSTEM ""file:///etc/hostname""> ]>" & vbLf &
        "<catalog><owner>&owner;</owner><file>&big;</file></catalog>"

    Sub Main()
        ' XmlReader prohibits DTDs by default.
        Try
            Using reader As XmlReader = XmlReader.Create(New StringReader(WithDtd))
                Do While reader.Read()
                Loop
            End Using
        Catch ex As XmlException
            Console.WriteLine("Default XmlReader: " & ex.Message)
        End Try

        ' XmlDocument.LoadXml has no resolver, so internal entities work and external entities do not load.
        Dim doc As New XmlDocument()
        Try
            doc.LoadXml(WithDtd)
            Console.WriteLine($"XmlDocument.LoadXml: owner=[{doc.SelectSingleNode("//owner").InnerText}] file=[{doc.SelectSingleNode("//file").InnerText}]")
        Catch ex As XmlException
            Console.WriteLine("XmlDocument.LoadXml: " & ex.Message)
        End Try

        ' Allowing DTD processing without a resolver still does not fetch external data.
        Dim settings As New XmlReaderSettings With {.DtdProcessing = DtdProcessing.Parse, .XmlResolver = Nothing}
        Using reader As XmlReader = XmlReader.Create(New StringReader(WithDtd), settings)
            Try
                Do While reader.Read()
                Loop
                Console.WriteLine("Parse with no resolver: finished")
            Catch ex As XmlException
                Console.WriteLine("Parse with no resolver: " & ex.Message)
            End Try
        End Using

        ' Entity expansion limits (billion laughs).
        Dim bomb As String = "<?xml version=""1.0""?><!DOCTYPE l [<!ENTITY a ""aaaaaaaaaa""><!ENTITY b ""&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;""><!ENTITY c ""&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;""><!ENTITY d ""&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"">]><l>&d;</l>"
        Dim limited As New XmlReaderSettings With {.DtdProcessing = DtdProcessing.Parse, .MaxCharactersFromEntities = 500}
        Try
            Using reader As XmlReader = XmlReader.Create(New StringReader(bomb), limited)
                Do While reader.Read()
                Loop
            End Using
        Catch ex As XmlException
            Console.WriteLine("MaxCharactersFromEntities: " & ex.Message)
        End Try
    End Sub
End Module
Default XmlReader: For security reasons DTD is prohibited in this XML document. To enable DTD processing set the DtdProcessing property on XmlReaderSettings to Parse and pass the settings into XmlReader.Create method.
XmlDocument.LoadXml: owner=[Shelf Library] file=[]
Parse with no resolver: finished
MaxCharactersFromEntities: The input document has exceeded a limit set by MaxCharactersFromEntities.

Rules: leave DtdProcessing at Prohibit for any document you did not write; if a DTD is genuinely needed, use Parse with XmlResolver = Nothing and MaxCharactersFromEntities; never set a resolver with credentials for untrusted input; DataContractSerializer and XmlSerializer are safe in this respect because they do not process DTDs. Code that targets .NET Framework 4.5.2 or earlier has different defaults (the resolver was on), which is one more reason to review migrated code.

BinaryFormatter is gone

BinaryFormatter wrote a .NET object graph, including type names, into a binary stream, and read it back by creating whatever types the stream named. That power is the vulnerability: deserializing an attacker-controlled stream can run attacker-chosen code, and no SerializationBinder or settings change can make it safe. The official guidance is that BinaryFormatter is dangerous and not recommended for data processing, even for data you believe to be trusted. Microsoft removed it in steps:

Release Change (from the official migration guide and the SYSLIB0011 diagnostic page)

.NET 5

BinaryFormatter.Serialize and Deserialize marked obsolete; using them reports SYSLIB0011.

.NET 8

In most project types the methods throw NotSupportedException unless a compatibility switch is set.

.NET 9

The in-box implementation is removed: the types and methods still exist so that code compiles, but they always throw PlatformNotSupportedException, and the old compatibility switches no longer help.

An unsupported compatibility package named System.Runtime.Serialization.Formatters replaces the throwing implementation for applications that cannot migrate yet; the guide recommends migrating instead.

The first program shows what the compiler does with new code: the diagnostic is SYSLIB0011, a warning that TreatWarningsAsErrors makes an error.

Option Strict On
Imports System.Runtime.Serialization.Formatters.Binary

Public Module S08
    <Serializable>
    Public Class Book
        Public Title As String = "Dune"
    End Class

    Sub Main()
        Dim formatter As New BinaryFormatter()
        Using stream As New System.IO.MemoryStream()
            formatter.Serialize(stream, New Book())
        End Using
    End Sub
End Module
S08.vb(11,30): error SYSLIB0011: 'BinaryFormatter' is obsolete: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.'. (https://aka.ms/dotnet-warnings/SYSLIB0011)

The second program acknowledges the diagnostic with #Disable Warning and shows the runtime behaviour on .NET 10, which is what old code or an old library meets after an upgrade.

Option Strict On
Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary

' The type still exists so that old binaries load, but since .NET 9 its implementation always throws.
Public Module S08b
    <Serializable>
    Public Class Book
        Public Title As String = "Dune"
    End Class

    Sub Main()
#Disable Warning SYSLIB0011 ' acknowledge the obsolete-API diagnostic, for this demonstration only
        Dim formatter As New BinaryFormatter()
        Using stream As New MemoryStream()
            Try
                formatter.Serialize(stream, New Book())
                Console.WriteLine("Serialized")
            Catch ex As PlatformNotSupportedException
                Console.WriteLine($"Serialize: {ex.GetType().Name}: {ex.Message}")
            End Try
        End Using
        Using stream As New MemoryStream(New Byte() {0, 1, 0, 0, 0})
            Try
                formatter.Deserialize(stream)
            Catch ex As PlatformNotSupportedException
                Console.WriteLine($"Deserialize: {ex.GetType().Name}")
            End Try
        End Using
#Enable Warning SYSLIB0011
    End Sub
End Module
Serialize: PlatformNotSupportedException: BinaryFormatter serialization and deserialization have been removed. See https://aka.ms/binaryformatter for more information.
Deserialize: PlatformNotSupportedException

<Serializable> still compiles, because the attribute belongs to the runtime and is also used by other mechanisms (ISerializable and the Exception classes), but on its own it no longer does anything useful. Existing data written with BinaryFormatter can be read without resurrecting the formatter: the migration guide describes a reader for the BinaryFormatter wire format that never instantiates the types in the payload, which is the only safe way to convert old files once.

If the data is…​ Use

Read by other systems, or must be human-readable

System.Text.Json, with an explicit schema

XML with a defined schema

XmlSerializer, or DataContractSerializer when WCF-style contracts are in use

Binary and performance-critical

A schema-based binary format such as protocol buffers or MessagePack (third-party libraries, named in the migration guide)

A private cache of your own types

JSON or a database; rebuild the cache when the format changes

Old BinaryFormatter data

One-off conversion with the payload reader, then a supported format

Sent between your own .NET processes

A contract type serialized with System.Text.Json, not an arbitrary object graph

The security of deserialization

Deserializing is parsing, and parsing untrusted input is the classic way into an application. The weakness catalogue calls it CWE-502, and it appears on the OWASP lists. The safe serializers above are safe because they constrain what the input can say; the checklist keeps that property:

  • Deserialize into a fixed contract type (a class you wrote with simple properties), not into Object or an interface chosen by the input.

  • Allow only registered derived types (<JsonDerivedType>, <XmlInclude>, <KnownType>); never read a type name from the data and load it.

  • Limit the size of the input before parsing (the request size limit, the file size) and keep MaxDepth at its default; XML has MaxCharactersFromEntities and MaxCharactersInDocument.

  • Keep DTD processing off and resolvers null.

  • Validate the object after deserializing: ranges, required members, relationships; the serializer checks the shape, not the meaning.

  • Do not trust a file because it is local: files are written by other programs, restored from backups, and edited by hand.

  • Treat deserialization errors as expected input: catch JsonException, InvalidOperationException (XML) and SerializationException (data contract), log the position and not the payload, and return a clear failure.

  • Never put secrets in a serialized object that you log or send; mark them <JsonIgnore>, <XmlIgnore> or leave them out of the data contract.

Differences from C#

Topic Visual Basic C#

Custom JsonConverter(Of T)

Not possible: Utf8JsonReader is unusable (BC30668)

Supported

JSON source generation

Not run for Visual Basic projects; use a C# project

Supported

Records

No record types; use a class with properties

record types serialize naturally

Required members

Cannot be declared (Visual Basic enforces those of C# types); <JsonRequired> works

required keyword

Attribute syntax

<JsonPropertyName("x")>, named arguments :=

[JsonPropertyName("x")], named arguments =

Default module accessibility

Classes in a Module are not public unless the module is Public; XML serializers reject them

No such trap

Async streams

DeserializeAsyncEnumerable is driven by hand (no Await For Each)

await foreach

XML literals

Dim x = <book id="1">...</book> builds an XElement

Not available

Anonymous types in JSON

New With {.Title = "Dune"} serializes like a class

Same

Where the books are outdated

The books use BinaryFormatter for binary persistence of object graphs. It was removed from the runtime in .NET 9: on .NET 9 and 10 that code compiles with a warning (an error under TreatWarningsAsErrors) and throws PlatformNotSupportedException when it runs, so the advice must be replaced by one of the supported serializers in the table above. Their coverage of DataContractSerializer as an alternative remains correct, and is covered here with the versioning rules and the dangers of element order. The books also show the .NET Framework forms of XML and web calls; XmlReader.Create with explicit settings is the current way to create a reader, and the security defaults described above are the ones to rely on.

Errors the compiler and runtime report

Message Cause and fix

BC30668: 'Utf8JsonReader' is obsolete: 'Types with embedded references are not supported in this version of your compiler.'

The type cannot be used in Visual Basic; see the converter section.

BC30610: Class 'ShelfJsonContext' must either be declared 'MustInherit' or override the following inherited 'MustOverride' member(s)

No source generator ran; the context must be generated by a C# project.

SYSLIB0011: 'BinaryFormatter' is obsolete

Replace it. A warning by default, an error with TreatWarningsAsErrors.

PlatformNotSupportedException: BinaryFormatter serialization and deserialization have been removed

Runtime behaviour on .NET 9 and later.

InvalidOperationException: ... is inaccessible due to its protection level. Only public types can be processed.

XmlSerializer over a non-public type, often a class in a default Friend module.

InvalidOperationException: ... cannot be serialized because it does not have a parameterless constructor

XmlSerializer needs Public Sub New().

JsonException: ... could not be mapped to any .NET member

Disallow is on and the input has an unknown member.

JsonException: The maximum configured depth of 64 has been exceeded

Input is nested deeper than MaxDepth.

SerializationException: ... 'Element' ... is not expected. Expecting element ...

A data contract document with members out of order, or a required one missing.

XmlException: For security reasons DTD is prohibited in this XML document

The input has a DOCTYPE; leave it prohibited unless the document is trusted.

Pitfalls checklist

  • Reuse JsonSerializerOptions and XmlSerializer instances.

  • System.Text.Json ignores public fields and private members by default and is case-sensitive on input.

  • A Date? that is Nothing is null in JSON and xsi:nil in XML; a Date that is Nothing is 0001-01-01.

  • Utf8JsonReader is unusable and JsonConverter(Of T) cannot be overridden in Visual Basic; use a surrogate property or a small C# helper.

  • The source generator does not run for Visual Basic projects.

  • Classes in a default Module are not public: XmlSerializer rejects them, and the data contract name includes the module name.

  • A data contract’s element order matters; an out-of-order optional member is dropped without an error.

  • IExtensibleDataObject keeps unknown content through a read and write; without it the newer data is lost.

  • XPath on a default-namespace document needs a namespace manager; XPathNavigator.Evaluate returns Double.

  • Do not deserialize into Object or arbitrary types from untrusted input; use fixed contracts and registered derived types.

  • BinaryFormatter is removed; migrate stored data once and never write it again.

  • Leave DTD processing prohibited.

  • Catch the serializer’s exception types, log the position and not the payload.