JSON — JavaScript Object Notation

JSON Handling in .NET

Serializing and deserializing JSON data

Andre Lopes
Checkout.com-techblog

--

Photo by Fotis Fotopoulos on Unsplash

Hi people!

JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for humans to read and write and for machines to parse and generate. It is commonly used for transmitting data between a server and a web application as an alternative to XML, and has many business applications. Many different programming languages use JSON which is a great way to ensure that different systems can work together and understand each other’s data.

In .NET, we have two most common libraries to handle JSON:

  • Newtonsoft.Json — a third-party powerful library with many features, including support for complex object graphs, custom converters, and more.
  • System.Text.Json — a high-performance JSON serialization and deserialization library introduced by Microsoft in .NET Core 3.

In this article, we’ll cover the use of System.Text.Json, which is the built-in library supported by Microsoft.

Let’s start

To quickly serialize an object to JSON, you just need to add the System.Text.Json namespace and to call the JsonSerializer.Serialize method.

using System.Text.Json;

var person = new Person { Name = "Luke", Age = 30 };

string json = JsonSerializer.Serialize(person);
// {"Name":"John","Age":30}

class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

Now, to deserialize a JSON, you just have to call JsonSerialize.Deserialize:

using System.Text.Json;

string json = """{"Name":"John","Age":30}""";
var person = JsonSerializer.Deserialize<Person>(json);

class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

Simple as that.

Now, you can also configure some behaviors of the serializer by passing a few options. Like, case insensitive property serialization, ignore null values, and others:

using System.Text.Json;
using System.Text.Json.Serialization;

var json = """{"name":"Luke","age":30}""";

var options1 = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

var person = JsonSerializer.Deserialize<Person>(json, options1);

person.Name = null;

var jsonResponse1 = JsonSerializer.Serialize(person, options1);
// { "Name": null, "Age": 30 }

var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};

var jsonResponse2 = JsonSerializer.Serialize(person, options);
// { "Age": 30 }

class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

Note that it can properly deserialize the JSON to Person object, but when you serialize it back to JSON, it uses PascalCase. This is because that is the default naming policy used by the library.

Naming policies

When serializing or deserializing JSON data, you may encounter situations where the property names in your object do not match the names in the JSON data. For example, your object may use camel case naming conventions, but the JSON data uses snake case naming conventions.

System.Text.Json provides naming policies to handle these situations, a set of rules that determine how property names are mapped between the JSON data and the object being serialized or deserialized.

var json = """{"name":"Luke","age":3}""";

var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var person = JsonSerializer.Deserialize<Person>(json, options);

string json2 = JsonSerializer.Serialize(person, options);
// { "name": "Luke", age: 30 }

class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

Unfortunately, .NET 7 still only has built-in CamelCase JSON naming policies, but it allows you to create your custom policies, like the one below for snake_case:

using System.Text;
using System.Text.Json;

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name)
{
if (string.IsNullOrEmpty(name))
{
return name;
}

var builder = new StringBuilder();
for (int i = 0; i < name.Length; i++)
{
if (char.IsUpper(name[i]))
{
if (i > 0 && name[i - 1] != '_')
{
builder.Append('_');
}

builder.Append(char.ToLower(name[i]));
}
else
{
builder.Append(name[i]);
}
}

return builder.ToString();
}
}

And to use, you just instantiate a new object from the class:

var json = """{"first_name":"Luke","person_age":3}""";

var options = new JsonSerializerOptions
{
PropertyNamingPolicy = new SnakeCaseNamingPolicy(),
};

var person = JsonSerializer.Deserialize<Person>(json, options);

string json2 = JsonSerializer.Serialize(person, options);
// { "first_name": "Luke", "person_age": 30 }

class Person
{
public string FirstName { get; set; }
public int PersonAge { get; set; }
}

Custom property serialization

Now, if you need to customize the serialization and deserialization of a property, System.Text.Json offers many built-in attributes that you can use to achieve your required customization. Here I’ll talk about a few of them.

JsonPropertyName

In case you need to handle a property name that does not match the names in the JSON data, or if you don’t want to use a naming policy to globally modify the mapping of all the JSON properties, you can make use of custom property name serialization with the JsonPropertyName attribute:

class Person
{
public string Name { get; set; }

[JsonPropertyName("YearsLived")]
public int Age { get; set; }
}

In the example above, the class property Age is now mapped to the JSON property YearsLived and vice-versa. So we can have it like this now:

using System.Text.Json;
using System.Text.Json.Serialization;

var json = """{"Name":"Luke","YearsLived":30}""";

var person = JsonSerializer.Deserialize<Person>(json);

var jsonSerialized = JsonSerializer.Serialize(person);
// {"Name":"Luke","YearsLived":30}

JsonRequired

If you need a property present during the deserialization process, you can mark the property with the JsonRequired attribute. If the property is not present, it will throw JsonException.

class Person
{
public string Name { get; set; }

[JsonRequired]
public int Age { get; set; }
}

JsonIgnore

When you need to ignore a property during serialization, you can mark it with the attribute JsonIgnore.

This attribute takes effect regardless of the DefaultIgnoreCondition I demonstrated above or any other global settings for ignoring properties.

class Person
{
public string Name { get; set; }

[JsonIgnore]
public int Age { get; set; }
}

This property also allows you to ignore based on conditions, or not ignore at all, by passing a JsonIgnoreCondition to it.

class Person
{
public string Name { get; set; }

// Will always serialize this, event if the option DefaultIgnoreCondition = JsonIgnoreCondition.Always
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public int Age { get; set; }

// Will never serialize this, event if the option DefaultIgnoreCondition = JsonIgnoreCondition.Never
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public int Height { get; set; }

// Will not serialize this if the default value is set. For this case, integer, is 0.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Weight { get; set; }

// Will not serialize this if the value is null
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string HairColor { get; set; }
}

WebAPIs

For WebAPIs, you can configure a global JSON configuration that will be applied to all serialization methods by default, including the endpoint request and response serialization. You can do that with the ConfigureHttpJsonOptions in the API configuration methods.

Note that by default, .NET WebAPI has the camel case name policy for serialization and deserialization.

using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.ConfigureHttpJsonOptions(option =>
{
option.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
option.SerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy();
});

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseSwagger();
app.UseSwaggerUI();

app.MapPost("/json", ([FromBody] Person person) =>
{
return new Person("Luke", null, 30);
});

app.Run();

record Person(string FirstName, string? LastName, int Age);

And if you want Swashbuckle to use your JSON configurations correctly, you have to configure the Microsoft.AspNetCore.Mvc.JsonOptions.

builder.Services.Configure<JsonOptions>(option =>
{
option.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
option.JsonSerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy();
});

Some other use cases

One case is if you need to read the response from an HTTP request from your client:

using System.Text.Json;
using System.Text.Json.Serialization;

var httpClient = new HttpClient(); // Rough implementation
var response = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/posts/1");

var responseBody = await response.Content.ReadAsStringAsync();

var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

var post = JsonSerializer.Deserialize<Post>(responseBody, jsonOptions);

Console.WriteLine($"Received post with title '{post.Title}' and body '{post.Text}'");

public class Post
{
public int UserId { get; set; }

public int Id { get; set; }

public string Title { get; set; }

[JsonPropertyName("body")]
public string Text { get; set; }
}

Or if you need to read from a JSON file, maybe for application configuration:

string configFile = "config.json";
if (File.Exists(configFile))
{
var configFileContents = File.ReadAllText(configFile);
var config = JsonSerializer.Deserialize<Configuration>(configFileContents);
}
else
{
Console.WriteLine($"Config file '{configFile}' not found");
}

public class Configuration
{
public string ApiUrl { get; set; }
public string ApiKey { get; set; }
public int MaxResults { get; set; }
public bool LogEnabled { get; set; }
}

The configuration file:

{
"apiUrl": "https://api.example.com",
"apiKey": "myapikey",
"maxResults": 10,
"logEnabled": true
}

These are just some of the many cases in which you might need to use JSON handling.

Conclusion

In this article, we saw how easy it is to handle JSON serialization and deserialization with System.Text.Json library.

You could also easily configure it to handle different name cases and options, like ignoring null values and serializing to JSON. And when you need some specific property configuration, you can make use of custom attributes provided by the library, like JsonPropertyName to set the name to be serialized to and deserialized from.

And last, we saw how it is simple to globally configure your JSON configurations in a .NET 7 minimal API.

Happy coding! 💻

--

--

Andre Lopes
Checkout.com-techblog

Full-stack developer | Casual gamer | Clean Architecture passionate