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 |
|---|---|---|
|
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 |
|
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 |
|
XML (and JSON through |
Strict element order; verbose namespaces; not for hand-written XML schemas |
|
You read and write the document yourself; XPath and LINQ to XML |
More code, no object mapping |
|
JSON you navigate or build yourself |
|
|
Binary object graphs of any type |
Removed: they throw |
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
Source: .NET: System.Text.Json overview
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 |
|---|---|
|
Pretty-printed output with two-space indentation. |
|
|
|
Match JSON names to properties regardless of case when reading. |
|
|
|
Include public fields as well as properties. |
|
Add |
|
Accept JSON that has trailing commas or comments, which strict JSON does not allow. |
|
|
|
|
|
Maximum nesting depth when reading and writing; the default is 64. |
|
|
|
Where type metadata comes from: reflection by default, a generated context when you supply one. |
|
By default non-ASCII and some HTML-sensitive characters are written as escapes; |
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 |
|---|---|
|
Name in JSON. |
|
Never write/read, or skip when |
|
Include a field or a property with a non-public accessor. |
|
Position in the output (lower first, default 0). |
|
The member must be present in the input; absent means |
|
The constructor to use when there is no parameterless one; parameters match JSON names, case-insensitively. |
|
A |
|
Use a specific converter; see the next section for why Visual Basic cannot define one. |
|
Opt in to type discriminators for a base class (see the polymorphism section). |
|
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.'.
Source: .NET API: Utf8JsonReader Struct
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
JsonDocumentorJsonNodeand 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.
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
<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>
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
{
}
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));
}
<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>
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 |
|---|---|
|
Name and namespace of the root element. |
|
Name of the element for a property; on a collection it removes the wrapper element, so each item becomes a sibling. |
|
Write the property as an attribute. |
|
Wrapper element and item name for a collection. |
|
Skip the member. |
|
The property is the element’s text content. |
|
Declare a derived type that may appear where the base type is declared. |
|
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 & Bob <team></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 |
|
.NET 8 |
In most project types the methods throw |
.NET 9 |
The in-box implementation is removed: the types and methods still exist so that code compiles, but they always throw |
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 |
|
XML with a defined schema |
|
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 |
One-off conversion with the payload reader, then a supported format |
Sent between your own .NET processes |
A contract type serialized with |
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
Objector 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
MaxDepthat its default; XML hasMaxCharactersFromEntitiesandMaxCharactersInDocument. -
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) andSerializationException(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 |
Not possible: |
Supported |
JSON source generation |
Not run for Visual Basic projects; use a C# project |
Supported |
Records |
No |
|
Required members |
Cannot be declared (Visual Basic enforces those of C# types); |
|
Attribute syntax |
|
|
Default module accessibility |
Classes in a |
No such trap |
Async streams |
|
|
XML literals |
|
Not available |
Anonymous types in JSON |
|
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 |
|---|---|
|
The type cannot be used in Visual Basic; see the converter section. |
|
No source generator ran; the context must be generated by a C# project. |
|
Replace it. A warning by default, an error with |
|
Runtime behaviour on .NET 9 and later. |
|
|
|
|
|
|
|
Input is nested deeper than |
|
A data contract document with members out of order, or a required one missing. |
|
The input has a |
Pitfalls checklist
-
Reuse
JsonSerializerOptionsandXmlSerializerinstances. -
System.Text.Jsonignores public fields and private members by default and is case-sensitive on input. -
A
Date?that isNothingisnullin JSON andxsi:nilin XML; aDatethat isNothingis0001-01-01. -
Utf8JsonReaderis unusable andJsonConverter(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
Moduleare not public:XmlSerializerrejects 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.
-
IExtensibleDataObjectkeeps 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.EvaluatereturnsDouble. -
Do not deserialize into
Objector arbitrary types from untrusted input; use fixed contracts and registered derived types. -
BinaryFormatteris 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.
References
-
System.Text.Json overview, How to serialize and deserialize JSON, Configure options, Customize properties, Polymorphism, Handle missing members
-
DataContractSerializer Class, Using data contracts, Forward-compatible data contracts
-
XmlReader Class, XmlWriter Class, XmlDocument Class, XPathNavigator Class
-
Process XML data using the XPath data model and XPath queries and namespaces
-
BinaryFormatter migration guide, BinaryFormatter security guide and SYSLIB0011: BinaryFormatter serialization is obsolete