DEV Community

Cover image for C# System.Text.Json
Karen Payne
Karen Payne

Posted on • Updated on

C# System.Text.Json

razor pages

Introduction

When working with json using strong typed classes and perfect json using System.Text.Json functionality for the most part is easy although there can be roadblocks which this article will address.

Official documentation

Microsoft has documented working with json in the following two links, serialize and deserialize json which is well worth taking some time to review.

Using these two links is where things started for the following topics. Even with great documentation there are still things that need to be drill down into.

GitHub Source code

JsonSerializerOptions

JsonSerializerOptions is a class that provides a way to specify various serialization and deserialization behaviors.

For ASP.NET Core

services.AddControllers()  
 .AddJsonOptions(options => { ... });
Enter fullscreen mode Exit fullscreen mode

Example

builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
    options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
Enter fullscreen mode Exit fullscreen mode

Both of the above will be discussed later.

In the code sample provided, some samples will have options defined in the method for ease of working things out while others will use options from a class.

Example with options in the method

Working with lowercased property names

public static void CasingPolicy()
{

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

    List<Product>? list = JsonSerializer.Deserialize<List<Product>>(json, options);

}
Enter fullscreen mode Exit fullscreen mode

While the proper way would be

public static void CasingPolicy()
{
    List<Product>? list = JsonSerializer.Deserialize<List<Product>>(json, JsonHelpers.LowerCaseOptions);
}
Enter fullscreen mode Exit fullscreen mode

JsonHelpers is a class in a separate class project with several predefined configurations.

public class JsonHelpers
{
    public static JsonSerializerOptions CaseInsensitiveOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };
    public static readonly JsonSerializerOptions WebOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true
    };
    public static JsonSerializerOptions WithWriteIndentOptions = new()
    {
        WriteIndented = true
    };

    public static JsonSerializerOptions WithWriteIndentAndIgnoreReadOnlyPropertiesOptions = new()
    {
        WriteIndented = true, IgnoreReadOnlyProperties = true
    };

    public static JsonSerializerOptions EnumJsonSerializerOptions = new() 
        { Converters = { new JsonStringEnumConverter() }, WriteIndented = true };

    public static JsonSerializerOptions LowerCaseOptions = new() 
        { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };

    public static List<T>? Deserialize<T>(string json)
        => JsonSerializer.Deserialize<List<T>>(json, WebOptions);

    public class LowerCaseNamingPolicy : JsonNamingPolicy
    {
        public override string ConvertName(string name) => name.ToLower();
    }

}
Enter fullscreen mode Exit fullscreen mode

And for desktop typically set up at class level as a static read-only property.

Class property casing

Most times when deserializing json property names are in the following format, Id, FirstName, LastName,BirthDate etc but what if json is id, firstname, lastname, birthdate?

For this we are working with the following model

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

And are receiving the following json.

[
  {
    "name": "iPhone max",
    "id": 1
  },
  {
    "name": "iPhone case",
    "id": 2
  },
  {
    "name": "iPhone ear buds",
    "id": 3
  }
]
Enter fullscreen mode Exit fullscreen mode

Code

  • SerializeLowerCasing method generates the json shown above
  • DeserializeLowerCasing method deserializes the json above and display the json to a console window

Important
The deserialization option must, in this case match the same options as when serialized but let's look at it as matching the options from an external source.

public static void CasingPolicy()
{
    var json = SerializeLowerCasing();

    DeserializeLowerCasing(json);

}

public static string SerializeLowerCasing()
{
    return JsonSerializer.Serialize(
        new List<Product>
        {
            new() { Id = 1, Name = "iPhone max"},
            new() { Id = 2, Name = "iPhone case" },
            new() { Id = 3, Name = "iPhone ear buds" }
        }, JsonHelpers.LowerCaseOptions);
}

public static void DeserializeLowerCasing(string json)
{
    List<Product>? products = JsonSerializer.Deserialize<List<Product>>(json, JsonHelpers.LowerCaseOptions);
    WriteOutJson(json);

    Console.WriteLine();
    Console.WriteLine();
    foreach (var product in products)
    {
        Console.WriteLine($"{product.Id,-3}{product.Name}");
    }
}
Enter fullscreen mode Exit fullscreen mode

results of serialize and deserialize operations

Working with Enum

Its common place to use an Enum to represent options for a property, for this there is the JsonStringEnumConverter class which converts enumeration values to and from strings.

Example using the following model.

public class PersonWithGender
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Gender Gender { get; set; }
    public override string ToString() => $"{Id,-4}{FirstName,-12} {LastName}";
}
Enter fullscreen mode Exit fullscreen mode

Option for this is in the class JsonHelpers.

public static JsonSerializerOptions EnumJsonSerializerOptions = new()
{
    Converters = { new JsonStringEnumConverter() }, 
    WriteIndented = true
};
Enter fullscreen mode Exit fullscreen mode

Usage

public static void WorkingWithEnums()
{

    List<PersonWithGender> people = CreatePeopleWithGender();
    var json = JsonSerializer.Serialize(people, JsonHelpers.EnumJsonSerializerOptions);

    WriteOutJson(json);
    List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(
        json, 
        JsonHelpers.EnumJsonSerializerOptions);

    Console.WriteLine();
    Console.WriteLine();
    list.ForEach(Console.WriteLine);
}
Enter fullscreen mode Exit fullscreen mode

Results

output from working with an enum

Note, if deserialization is missing the options a runtime exception is thrown.

deserialization missing options

If options are not defined for serialization the numeric values are provided, not the actual Enum member.

serialization missing options

With ASP.NET Core and Razor Pages using the same model we can serialize and deserialize as done with desktop.

public class JsonSamples
{
    public List<PersonWithGender> CreatePeopleWithGender() =>
    [
        new() { Id = 1, FirstName = "Anne", LastName = "Jones", Gender = Gender.Female },
        new() { Id = 2, FirstName = "John", LastName = "Smith", Gender = Gender.Male },
        new() { Id = 3, FirstName = "Bob", LastName = "Adams", Gender = Gender.Unknown }
    ];

    public void WorkingWithEnums()
    {
        var options = new JsonSerializerOptions
        {
            Converters = { new JsonStringEnumConverter() },
            WriteIndented = true
        };

        List<PersonWithGender> people = CreatePeopleWithGender();
        var json = JsonSerializer.Serialize(people);
        List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(json);
    }

}
Enter fullscreen mode Exit fullscreen mode

Or use the method in JsonHelpers class

public void WorkingWithEnums()
{


    List<PersonWithGender> people = CreatePeopleWithGender();
    var json = JsonSerializer.Serialize(people, 
        JsonHelpers.EnumJsonSerializerOptions);
    List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(json, 
        JsonHelpers.EnumJsonSerializerOptions);
}
Enter fullscreen mode Exit fullscreen mode

To get the following.

[
  {
    "Id": 1,
    "FirstName": "Anne",
    "LastName": "Jones",
    "Gender": "Female"
  },
  {
    "Id": 2,
    "FirstName": "John",
    "LastName": "Smith",
    "Gender": "Male"
  },
  {
    "Id": 3,
    "FirstName": "Bob",
    "LastName": "Adams",
    "Gender": "Unknown"
  }
]
Enter fullscreen mode Exit fullscreen mode

The other option is through adding options through WebApplicationBuilder in Program.cs

builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
    options.JsonSerializerOptions.WriteIndented = true;
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
Enter fullscreen mode Exit fullscreen mode

Then alter the last method.

public void WorkingWithEnums(JsonOptions options)
{

    List<PersonWithGender> people = CreatePeopleWithGender();
    var json = JsonSerializer.Serialize(people, 
        options.JsonSerializerOptions);
    List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(json, 
        options.JsonSerializerOptions);
}
Enter fullscreen mode Exit fullscreen mode

In this case, in index.cshtml.cs we setup the options using dependency injection.

public class IndexModel : PageModel
{
    private readonly IOptions<JsonOptions> _options;

    public IndexModel(IOptions<JsonOptions> options)
    {
        _options = options;
    }
Enter fullscreen mode Exit fullscreen mode

Spaces in property names

There may be cases were json data has properties with spaces.

[
  {
    "Id": 1,
    "First Name": "Mary",
    "LastName": "Jones"
  },
  {
    "Id": 2,
    "First Name": "John",
    "LastName": "Burger"
  },
  {
    "Id": 3,
    "First Name": "Anne",
    "LastName": "Adams"
  }
]
Enter fullscreen mode Exit fullscreen mode

For this, specify the property name from json with JsonPropertyNameAttribute as shown below.

public class Person
{
    public int Id { get; set; }
    [JsonPropertyName("First Name")]
    public string FirstName { get; set; }
    [JsonPropertyName("Last Name")]
    public string LastName { get; set; }

    public string FullName => $"{FirstName} {LastName}";

    /// <summary>
    /// Used for demonstration purposes 
    /// </summary>
    public override string ToString() => $"{Id,-4}{FirstName, -12} {LastName}";

}
Enter fullscreen mode Exit fullscreen mode

C# Code (from provided code in a GitHub repository)

public static void ReadPeopleWithSpacesInPropertiesOoops()
{
    var people = JsonSerializer.Deserialize<List<Person>>(File.ReadAllText("Json\\people2.json"));
    foreach (var person in people)
    {
        Console.WriteLine(person);
    }
}
Enter fullscreen mode Exit fullscreen mode

For more details on this and more like hyphens in property names see the following well written Microsoft documentation.

Read string values as int

There may be cases were a json file is provided with your model expects an int.

[
  {
    "Id": "1",
    "Name": "iPhone max"
  },
  {
    "Id": "2",
    "Name": "iPhone case"
  },
  {
    "Id": "3",
    "Name": "iPhone ear buds"
  }
]
Enter fullscreen mode Exit fullscreen mode

Model where Id is an int but in json a string.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

For this, use in desktop projects or see below for another option using an attribute on the Id property.

var jsonOptions = new JsonSerializerOptions()
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};
Enter fullscreen mode Exit fullscreen mode

JsonSerializerOptions.NumberHandling indicates that gets or sets an object that specifies how number types should be handled when serializing or deserializing.

In this ASP.NET Core sample

public async Task<List<Product>> ReadProductsWithIntAsStringFromWeb()
{

    var json = await Utilities.ReadJsonAsync(
        "https://raw.githubusercontent.com/karenpayneoregon/jsonfiles/main/products.json");
    ProductsStringAsInt = json;

    return JsonSerializer.Deserialize<List<Product>>(json, JsonHelpers.WebOptions)!;

}
Enter fullscreen mode Exit fullscreen mode

JsonHelpers.WebOptions uses JsonSerializerDefaults.Web with the default to string quoted numbers as numeric.

Shows sonSerializerDefaults.Web defaults

Another method is to set an attribute for desktop or web.

public class Product
{
    [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
    public int Id { get; set; }
    public string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Save decimal as two decimal places

Given the following created using Bogus Nuget package.

screen shot

We want

[
  {
    "ProductId": 12,
    "ProductName": "Awesome Fresh Salad",
    "UnitPrice": "2.99",
    "UnitsInStock": 5
  },
  {
    "ProductId": 19,
    "ProductName": "Awesome Wooden Pizza",
    "UnitPrice": "7.28",
    "UnitsInStock": 3
  },
  {
    "ProductId": 15,
    "ProductName": "Ergonomic Concrete Gloves",
    "UnitPrice": "2.38",
    "UnitsInStock": 2
  }
]
Enter fullscreen mode Exit fullscreen mode

This is done using a custom converter as follows.

// Author https://colinmackay.scot/tag/system-text-json/
public class FixedDecimalJsonConverter : JsonConverter<decimal>
{
    public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string? stringValue = reader.GetString();
        return string.IsNullOrWhiteSpace(stringValue)
            ? default
            : decimal.Parse(stringValue, CultureInfo.InvariantCulture);
    }

    public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
    {
        string numberAsString = value.ToString("F2", CultureInfo.InvariantCulture);
        writer.WriteStringValue(numberAsString);
    }
}
Enter fullscreen mode Exit fullscreen mode

Code which in this case gets a list and saves to a json file which produces the output above.

List<ProductItem> results = products
    .Select<Product, ProductItem>(container => container).ToList();


if (results.Any())
{
    // process checked
    File.WriteAllText("Products.json", JsonSerializer.Serialize(results, new JsonSerializerOptions
    {
        WriteIndented = true,
        Converters = { new FixedDecimalJsonConverter() }
    }));
}
Enter fullscreen mode Exit fullscreen mode

Since the property UnitPrice is stored as a string we use the same technique already shown by setting NumberHandling = JsonNumberHandling.AllowReadingFromString.

// process checked
File.WriteAllText("Products.json", JsonSerializer.Serialize(results, new JsonSerializerOptions
{
    WriteIndented = true,
    Converters = { new FixedDecimalJsonConverter() }
}));


var jsonOptions = new JsonSerializerOptions()
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};
var json = File.ReadAllText("Products.json");
List<Product>? productsFromFile = JsonSerializer.Deserialize<List<Product>>(json, jsonOptions);
Enter fullscreen mode Exit fullscreen mode

Ignore property

There may be times when a property should not be included in serialization or deserialization.

Use JsonIgnore attribute, here BirthDate will be ignored.

public class PersonIgnoreProperty : Person1
{
    [JsonIgnore]
    public DateOnly BirthDate { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Which can be controlled with JsonIgnoreCondition Enum

public class PersonIgnoreProperty : Person1
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public DateOnly BirthDate { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Immutable types

By default, System.Text.Json uses the default public parameterless constructor. However, you can tell it to use a parameterized constructor, which makes it possible to deserialize an immutable class or struct.

The following demonstrates deserializing a struct where the constructor is decelerated with JsonConstructor attribute.

public readonly struct PersonStruct
{
    public int Id { get;  }

    public string FirstName { get; }

    public string LastName { get; }

    [JsonConstructor]
    public PersonStruct(int id, string firstName, string lastName) =>
        (Id, FirstName, LastName) = (id, firstName, lastName);

    /// <summary>
    /// Used for demonstration purposes 
    /// </summary>
    public override string ToString() => $"{Id, -4}{FirstName,-12}{LastName}";

}
Enter fullscreen mode Exit fullscreen mode

Deserializing from static json.

public static void Immutable()
{
    var json =
        """
        [
           {
             "Id": 1,
             "FirstName": "Mary",
             "LastName": "Jones"
           },
           {
             "Id": 2,
             "FirstName": "John",
             "LastName": "Burger"
           },
           {
             "Id": 3,
             "FirstName": "Anne",
             "LastName": "Adams"
           }
        ]
        """;

    var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
    List<PersonStruct>? peopleReadOnly = JsonSerializer.Deserialize<List<PersonStruct>>(json, options);
    peopleReadOnly.ForEach(peep => Console.WriteLine(peep));
}
Enter fullscreen mode Exit fullscreen mode

Results

Ignore null property values

You can ignore properties on serialization and deserialization using JsonIgnoreCondition Enum.

Suppose json data has an primary key which should be ignored when populating a database table using EF Core or that a gender property is not needed at all. The following first shows not ignoring properties Id and Gender while the second ignores Id and Gender.

Models

public class PersonWithGender
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Gender Gender { get; set; }
    public override string ToString() => $"{Id,-4}{FirstName,-12} {LastName}";
}


public class PersonWithGender1
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public Gender Gender { get; set; }
    public override string ToString() => $"{Id,-4}{FirstName,-12} {LastName}";
}
Enter fullscreen mode Exit fullscreen mode

Serializing data.

public static void IgnoreNullValues()
{
    PersonWithGender person1 = new() { FirstName = "Karen", LastName = "Payne", Gender = Gender.Female};
    var data1 = JsonSerializer.Serialize(person1, JsonHelpers.EnumJsonSerializerOptions);
    PersonWithGender1 person2 = new() { FirstName = "Karen", LastName = "Payne" };
    var data2 = JsonSerializer.Serialize(person2, JsonHelpers.WithWriteIndentOptions);
}
Enter fullscreen mode Exit fullscreen mode

Results

{
  "Id": 0,
  "FirstName": "Karen",
  "LastName": "Payne",
  "Gender": "Female"
}


{
  "FirstName": "Karen",
  "LastName": "Payne"
}
Enter fullscreen mode Exit fullscreen mode

Serializing to a dictionary

In this sample data is read from a Microsoft NorthWind database table Employees to a dictionary with the key as first and last name and the value as the primary key. Dapper is used for the read operation.

Important Before running this code create the database and populate with populate.sql in the script folder of the project ReadOddJsonApp.

internal class DapperOperations
{
    private IDbConnection db = new SqlConnection(ConnectionString());

    public void GetDictionary()
    {
        const string statement =
            """
                SELECT EmployeeID as Id, 
                     FirstName + ' ' + LastName AS FullName,
                     LastName
                FROM dbo.Employees ORDER BY LastName;
                """;

        Dictionary<string, int> employeeDictionary = db.Query(statement).ToDictionary(
            row => (string)row.FullName,
            row => (int)row.Id);


        Console.WriteLine(JsonSerializer.Serialize(employeeDictionary, JsonHelpers.WithWriteIndentOptions));
    }
}
Enter fullscreen mode Exit fullscreen mode

Serialize to dictionary data from a database table

Reuse JsonSerializerOptions instances

Microsoft's docs indicate: If you use JsonSerializerOptions repeatedly with the same options, don't create a new JsonSerializerOptions instance each time you use it. Reuse the same instance for every call.

In much of the code provided here violates the above as they are standalone code samples, best to follow above for applications.

Summary

This article provides information to handle unusual json formats and normal formatting as a resources with code samples located in a GitHub repository.

Source code

Clone the following GitHub repository.

Resource

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Karen Payne,
Your tips are very useful
Thanks for sharing