Serialization: JSON and XML
Serialization converts objects to a storable/transmittable format (JSON, XML, binary) and deserialization converts back.
Concept
Serialization converts objects to a storable/transmittable format (JSON, XML, binary) and deserialization converts back.
Serialize / deserialize JsonSerializer.Serialize(obj, options), JsonSerializer.Deserialize<T>(json, options)
WriteIndented, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition, NumberHandling, ReferenceHandler, Converters
[JsonPropertyName("...")], [JsonIgnore], [JsonRequired], [JsonConverter(...)], [JsonPropertyOrder] Enums as strings new JsonStringEnumConverter()
Derive from JsonConverter<T> and override Read/Write [JsonPolymorphic] + [JsonDerivedType(typeof(X), "discriminator")]
JsonDocument / JsonElement (read-only DOM), JsonNode (mutable DOM)
[JsonSerializable(typeof(T))] on a partial class : JsonSerializerContext — no run-time reflection, required for Native AOT
Reflection vs source generation: normal projects use reflection-based serialization by default. File-based apps (dotnet run file.cs) and Native AOT apps disable it, which is why the first example starts with #:property PublishAot=false; the second example uses source generation, which works everywhere and is faster.
Versioning: add properties as optional, never rename without [JsonPropertyName], ignore unknown members (default) or reject them (JsonUnmappedMemberHandling.Disallow).
Security: never deserialize untrusted data into arbitrary types (the old BinaryFormatter is removed from .NET for this reason); validate after deserializing; limit MaxDepth and payload sizes.
LINQ to XML (XDocument, XElement, XAttribute, XNamespace): build, query (Descendants, Elements), modify and save XML; explicit casts read values ((int)element).
XmlSerializer: maps public classes to XML with attributes like [XmlAttribute], [XmlArray], [XmlArrayItem], [XmlElement].
XPath (System.Xml.XPath) and XML namespaces are supported for legacy formats (AUTOSAR ARXML, ODX/PDX diagnostic data, many industrial configs).
Why It Matters
JSON is the language of web APIs, configuration and messaging; XML remains common in automotive and industrial tooling. Getting naming, versioning and security right avoids integration bugs and vulnerabilities.
Syntax
string json = JsonSerializer.Serialize(vehicle, options);
Vehicle? v = JsonSerializer.Deserialize<Vehicle>(json, options);
var doc = XDocument.Parse(xml);
var values = doc.Descendants("dtc").Select(e => e.Value);Example
28.1 JSON with options, polymorphism and a custom converter 28.2 Source-generated JSON (AOT-friendly)
28.3 XML with LINQ to XML and XmlSerializer
#:property PublishAot=false
// ^ file-based apps (dotnet run file.cs) default to Native AOT settings, which disable
// reflection-based JSON. A normal project (dotnet new console) does not need this line.
using System.Text.Json;
using System.Text.Json.Serialization;
var report = new TestReport(
Vin: "EHBAUREV250000001",
Results: [new TestResult("TC-001", Verdict.Pass, 1.25), new TestResult("TC-002", Verdict.Fail, 3.5) { Notes = "timeout" }],
CreatedUtc: new DateTime(2026, 3, 10, 8, 30, 0, DateTimeKind.Utc));
// Serialize with options
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
string json = JsonSerializer.Serialize(report, options);
Console.WriteLine(json);
// Deserialize
TestReport? back = JsonSerializer.Deserialize<TestReport>(json, options);
Console.WriteLine($"round-trip: {back!.Results.Count} results, second verdict {back.Results[1].Verdict}, equal VIN: {back.Vin == report.Vin}");
// Reading unknown JSON with JsonDocument
using JsonDocument doc = JsonDocument.Par
…using System.Text.Json;
using System.Text.Json.Serialization;
// Source-generated serialization: no reflection at run time, works with Native AOT and trimming
var status = new EcuStatus("VCU", 12.6, ["P0A80"]);
string json = JsonSerializer.Serialize(status, AppJsonContext.Default.EcuStatus);
Console.WriteLine(json);
EcuStatus back = JsonSerializer.Deserialize(json, AppJsonContext.Default.EcuStatus)!;
Console.WriteLine($"round-trip: {back.Code} {back.Voltage} V, {back.Dtcs.Length} DTC(s), equal: {back.Code == status.Code && back.Voltage == status.Voltage}");
public record EcuStatus(string Code, double Voltage, string[] Dtcs);
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(EcuStatus))]
internal partial class AppJsonContext : JsonSerializerContext { }using System.Xml.Linq;
using System.Xml.Serialization;
// LINQ to XML: build a document
XNamespace ns = "urn:example:diag";
var doc = new XDocument(
new XElement(ns + "vehicle", new XAttribute("vin", "EHBAUREV250000001"),
new XElement(ns + "ecu", new XAttribute("code", "VCU"), new XElement(ns + "dtc", "P0A80")),
new XElement(ns + "ecu", new XAttribute("code", "BMS")),
new XElement(ns + "ecu", new XAttribute("code", "ABS"), new XElement(ns + "dtc", "C1234"), new XElement(ns + "dtc", "U0100"))));
Console.WriteLine(doc);
// Query it with LINQ
var withFaults = doc.Descendants(ns + "ecu")
.Where(e => e.Elements(ns + "dtc").Any())
.Select(e => $"{(string)e.Attribute("code")!}: {string.Join(",", e.Elements(ns + "dtc").Select(d => d.Value))}");
Console.WriteLine($"ECUs with DTCs -> {string.Join(" | ", withFaults)}");
// Parse XML text and modify it
XElement config = XElement.Parse("""<config><timeout unit="ms">2000</timeout><retries>3</retries></config>""");
int timeout = (int)config.Element("timeout")!;
config.Element("retries")!.Value = "5";
config.Add(new XElement("logLevel", "Debug"));
Console.WriteLine($"timeout {timeout} {config.Element("timeout
…Expected Result
Source-generated JSON:
{
"vin": "EHBAUREV250000001",
"results": [
{
"id": "TC-001",
"verdict": "Pass",
"durationS": 1.25
},
{
"id": "TC-002",
"verdict": "Fail",
"durationS": 3.5,
"notes": "timeout"
}
],
"createdUtc": "2026-03-10T08:30:00Z"
}
round-trip: 2 results, second verdict Fail, equal VIN: True
JsonDocument: ecu BMS, fw 2.4.1, max cell 3.72
polymorphic: [{"kind":"analog","Value":12.6,"Name":"Voltage"},{"kind":"digital","State":true,"Name":"Ignition"}]
deserialized types: AnalogSignal, DigitalSignal
custom converter: {"Temperature":"21.5 C"}
JsonException: 'n' is an invalid start of a property name{"code":"VCU","voltage":12.6,"dtcs":["P0A80"]}
round-trip: VCU 12.6 V, 1 DTC(s), equal: True<vehicle vin="EHBAUREV250000001" xmlns="urn:example:diag">
<ecu code="VCU">
<dtc>P0A80</dtc>
</ecu>
<ecu code="BMS" />
<ecu code="ABS">
<dtc>C1234</dtc>
<dtc>U0100</dtc>
</ecu>
</vehicle>
ECUs with DTCs -> VCU: P0A80 | ABS: C1234,U0100
timeout 2000 ms; modified: <config><timeout unit="ms">2000</timeout><retries>5</retries><logLevel>Debug</logLevel></config>
<Ecu xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Code="VCU">
<Address>2016</Address>
<Faults>
<Dtc>P0A80</Dtc>
</Faults>
</Ecu>
deserialized: VCU 0x7E0 P0A80Common Mistakes
Running the reflection-based JSON example as a file-based app without #:property PublishAot=false — it fails with InvalidOperationException: Reflection-based serialization has been disabled for this application. Use the directive, a regular project, or source generation.
Mismatched property naming between client and server (PascalCase vs camelCase).
Serializing entities with circular references (use DTOs or ReferenceHandler.Preserve).
Using DateTime without a kind — prefer UTC or DateTimeOffset.
Deserializing untrusted polymorphic data into broad base types.
Building XML with string concatenation (escaping bugs) instead of XElement.
Best Practices
Create one shared JsonSerializerOptions instance (it caches metadata) or use source generation.
Serialize DTO records designed for the wire, not domain entities.
Use JsonDocument for exploring unknown JSON and typed deserialization for known contracts.
Validate required fields ([JsonRequired] or required members).
Interview Relevance
Medium–high: "System.Text.Json vs Newtonsoft.Json", "how to customize serialization?", "why was BinaryFormatter removed?", "polymorphic JSON", "source-generated serializers and AOT".
Practice Questions
Serialize a list of Dtc records to a file and read it back.
Rename a property in JSON with [JsonPropertyName("dtc_code")].
Parse a JSON array of readings with JsonDocument and compute the average.
Convert an XML list of ECUs into JSON.
Write a converter that serializes TimeSpan as total milliseconds.
Quick Quiz
Q1. (Multiple choice) Which option writes durationS instead of DurationS? A) WriteIndented B) PropertyNamingPolicy = JsonNamingPolicy.CamelCase C) IncludeFields D) MaxDepth Answer: B (verified).
Q2. (True/False) Source-generated JsonSerializerContext works with Native AOT.
Answer: True (verified in 28.2 under AOT-default settings).
Q3. (Predict the output) Which discriminator property appears in the polymorphic JSON?
Answer: "kind" with values analog and digital (verified).
Q4. (Find the bug) JsonSerializer.Deserialize<Report>("{ not json }") crashes the app. What should the code do?
Answer: Catch JsonException (as in the example) and report invalid input.
Coding Challenge
Serialize a record Config(string Port, int BaudRate, string[] Filters) to indented camelCase JSON with source generation, then deserialize it and print BaudRate.
using System.Text.Json;
using System.Text.Json.Serialization;
var cfg = new Config("COM3", 500_000, ["0x7E0", "0x7E8"]);
string json = JsonSerializer.Serialize(cfg, ConfigContext.Default.Config);
Console.WriteLine(json);
Console.WriteLine(JsonSerializer.Deserialize(json, ConfigContext.Default.Config)!.BaudRate);
record Config(string Port, int BaudRate, string[] Filters);
[JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Config))]
partial class ConfigContext : JsonSerializerContext { }