Json.NET
C# programming
data deserialization
custom converters
.NET development

Using Json.NET converters to deserialize properties

Master System Design with Codemia

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

Introduction

Json.NET converters are the right tool when a JSON value does not map cleanly to the target C# property type. Instead of spreading special-case parsing logic across your model classes, you can put the custom read behavior in one converter and attach it either to a property or to a type.

Why a Converter Is Useful

Default deserialization works well when JSON and C# shapes already match. It breaks down when:

  • a property can arrive as more than one JSON type
  • a string must be converted into a richer domain type
  • one property needs special parsing rules without changing the whole model

For example, imagine a field that is sometimes an integer and sometimes a quoted string:

json
{ "id": 42 }

or:

json
{ "id": "42" }

A custom converter lets one C# property handle both forms predictably.

A Property-Level Converter Example

Here is a converter that accepts either an integer token or a numeric string and always returns an int.

csharp
1using System;
2using Newtonsoft.Json;
3
4public sealed class FlexibleIntConverter : JsonConverter<int>
5{
6    public override int ReadJson(JsonReader reader, Type objectType, int existingValue, bool hasExistingValue, JsonSerializer serializer)
7    {
8        if (reader.TokenType == JsonToken.Integer)
9            return Convert.ToInt32(reader.Value);
10
11        if (reader.TokenType == JsonToken.String && int.TryParse((string?)reader.Value, out var value))
12            return value;
13
14        throw new JsonSerializationException($"Cannot convert token {reader.TokenType} to int.");
15    }
16
17    public override void WriteJson(JsonWriter writer, int value, JsonSerializer serializer)
18    {
19        writer.WriteValue(value);
20    }
21}

Attach it directly to the property:

csharp
1using Newtonsoft.Json;
2
3public sealed class UserDto
4{
5    [JsonProperty("id")]
6    [JsonConverter(typeof(FlexibleIntConverter))]
7    public int Id { get; set; }
8}

Now both JSON forms deserialize cleanly into Id.

Deserializing the Property

csharp
1using Newtonsoft.Json;
2
3var fromNumber = JsonConvert.DeserializeObject<UserDto>(@"{""id"":42}");
4var fromString = JsonConvert.DeserializeObject<UserDto>(@"{""id"":""42""}");
5
6Console.WriteLine(fromNumber!.Id); // 42
7Console.WriteLine(fromString!.Id); // 42

This is the most common Json.NET converter pattern: a property has a weird input contract, so the converter absorbs that inconsistency.

Property-Level vs Type-Level Registration

You can register converters in three common places:

  • directly on the property with [JsonConverter(...)]
  • on the target type itself
  • globally in JsonSerializerSettings

Property-level registration is the safest default when only one field needs special handling. Global registration is useful only when the behavior should apply everywhere the type appears.

csharp
var settings = new JsonSerializerSettings();
settings.Converters.Add(new FlexibleIntConverter());

Global converters are powerful, but they also make behavior more implicit. If another model later uses the same type with different expectations, global registration can become surprising.

Work With JToken When the Shape Is Complex

For more complex JSON, reading into a JToken first can make the converter clearer.

csharp
1using Newtonsoft.Json.Linq;
2
3public sealed class TagsConverter : JsonConverter<string[]>
4{
5    public override string[] ReadJson(JsonReader reader, Type objectType, string[] existingValue, bool hasExistingValue, JsonSerializer serializer)
6    {
7        var token = JToken.Load(reader);
8
9        return token.Type switch
10        {
11            JTokenType.Array => token.ToObject<string[]>() ?? Array.Empty<string>(),
12            JTokenType.String => new[] { token.ToObject<string>()! },
13            _ => throw new JsonSerializationException("Expected string or array.")
14        };
15    }
16
17    public override void WriteJson(JsonWriter writer, string[] value, JsonSerializer serializer)
18    {
19        serializer.Serialize(writer, value);
20    }
21}

This approach is easier when the incoming JSON shape varies more than the target C# type does.

Common Pitfalls

  • Registering a converter globally when only one property actually needs special handling.
  • Silently accepting malformed JSON and hiding real data-quality issues.
  • Forgetting to implement WriteJson consistently when the object must also be serialized back out.
  • Using a converter when a simple [JsonProperty] name mapping would have solved the problem.
  • Writing property-specific logic in a general-purpose converter and making later reuse confusing.

Summary

  • Json.NET converters are for properties or types that need custom read or write behavior.
  • Property-level converters are usually the clearest and safest option.
  • A converter can normalize multiple JSON token shapes into one C# property type.
  • 'JToken is useful when the incoming JSON shape varies significantly.'
  • Keep the converter narrow, explicit, and consistent with the domain rules it enforces.

Course illustration
Course illustration

All Rights Reserved.