-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
144: The System.Text.Json Advanced Features: Custom Converters
Ever hit a wall where System.Text.Json just refuses to play ball with a specific type? Most of the time, the built-in serializer is a beast—it handles strings, ints, and lists without you lifting a finger. But eventually, you'll encounter a legacy API or a specific data requirement where the JSON doesn't "shape" the way C# objects do. That's where custom converters come in.
Think of a custom converter like a specialized translator at a diplomatic summit. Most delegates speak a common trade language (that's your standard JSON serialization). But then you have one delegate who speaks an ancient, highly specific dialect. You can't just tell them to "speak standard"; they can't. Instead, you hire a translator who sits right next to them. When the delegate speaks, the translator converts it into the trade language for everyone else. When someone speaks to the delegate, the translator converts it back into that specific dialect. The rest of the summit doesn't even know the dialect exists; they just see the translated result.
In C#, the "dialect" is the weird JSON format, the "delegate" is your C# class, and the JsonConverter<T> is your translator.
Dealing with "Flattened" Data
Let's look at a real scenario. Imagine you're working with a mapping API. In your C# code, you want a Coordinate object because it's clean and type-safe. However, the API sends coordinates as a single string: "40.7128,-74.0060". If you try to deserialize that into a class with Latitude and Longitude properties, the serializer will throw an exception because it sees a string, not an object.
public class Coordinate
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
To fix this, we build the translator. We inherit from JsonConverter<Coordinate> and override two methods: Read (JSON to C#) and Write (C# to JSON).
using System.Text.Json;
using System.Text.Json.Serialization;
public class CoordinateConverter : JsonConverter<Coordinate>
{
// This handles the "Translation" from JSON string to C# Object
public override Coordinate Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? value = reader.GetString();
if (string.IsNullOrWhiteSpace(value)) return null;
var parts = value.Split(',');
return new Coordinate
{
Latitude = double.Parse(parts[0]),
Longitude = double.Parse(parts[1])
};
}
// This handles the "Translation" from C# Object to JSON string
public override void Write(Utf8JsonWriter writer, Coordinate value, JsonSerializerOptions options)
{
writer.WriteStringValue($"{value.Latitude},{value.Longitude}");
}
}
Wiring the Converter into the Pipeline
Just writing the converter isn't enough; the JsonSerializer doesn't automatically scan your project for every possible converter. You have to tell it to use yours. I usually see people do this in one of two ways, depending on how much control they want.
If you only want this converter to apply to one specific property in one specific class, you can use an attribute. This is the "surgical" approach:
public class UserProfile
{
public string Username { get; set; }
[JsonConverter(typeof(CoordinateConverter))]
public Coordinate LastKnownLocation { get; set; }
}
But if Coordinate is used in fifty different classes across your project, adding attributes everywhere is a nightmare. In that case, you add it to the global options. I personally prefer this for domain-wide types because it keeps your POCOs (Plain Old CLR Objects) clean of serialization logic.
var options = new JsonSerializerOptions();
options.Converters.Add(new CoordinateConverter());
string json = "{\"Username\": \"DevUser\", \"LastKnownLocation\": \"40.7128,-74.0060\"}";
var profile = JsonSerializer.Deserialize<UserProfile>(json, options);
One quick tip: when implementing Read, be careful with reader.GetString(). In a high-performance production environment, you might want to work with the ReadOnlySpan<byte> directly to avoid allocations, but for 95% of use cases, the string approach I showed you is perfectly fine and much easier to maintain.
📋 Practical Task
Exercise: Implementing a Monetary Value Converter
You are integrating with a financial API that represents currency as a combined string in the format "USD 125.50" or "EUR 99.00". Your C# application uses a Money record to keep the currency code and the amount separate for calculation purposes.
Your Goal: Create a custom JsonConverter that allows System.Text.Json to seamlessly convert between the API's string format and your C# record.
Requirements:
- Define a record:
public record Money(string Currency, decimal Amount); - Create a
MoneyConverterthat inherits fromJsonConverter<Money>. - The
Readmethod should split the string (e.g.,"USD 100.00") into theCurrency("USD") andAmount(100.00) parts. - The
Writemethod should combine the properties back into the"CUR Amount"format. - Demonstrate the converter working by serializing and deserializing a class called
Invoicewhich contains aMoney TotalAmountproperty.
There are no comments for now.