From C# Arithmetic Expressions to JSON and Back

From C# Arithmetic Expressions to JSON and Back

The previous blog post has introduced a calculator engine project. This post extends it to convert arithmetic expressions to and from JSON.

For example, an expression built like this:

Expression.CreateMultiValued([3, 3], new Addition())

could be represented in JSON as:

{
	"Operation": "Addition",
	"Subexpressions": [
		{
			"Value": 3
		},
		{
			"Value": 3
		}
	]
}

The objective is to implement an ExpressionJsonSerializer class with the following interface that does such conversions:

public interface IExpressionJsonSerializer
{
    public string Serialize(Expression expression);
    public Expression? Deserialize(string json);
}

Terminology

Serialisation is the process of converting a C# object into a format that can be stored or transmitted. Once converted, it can be written to a file, stored in a database, or sent over a network.

Deserialisation is the reverse process of converting serialised data back into its object form.

JSON (JavaScript Object Notation) is one among many suitable for serialisation data formats.

Map and Convert are more general terms than Serialise & Deserialise. They can refer to anything that becomes something else.

Plan

The objective is to implement an ExpressionJsonSerializer class with the following interface:

public interface IExpressionJsonSerializer
{
    public string Serialize(Expression expression);
    public Expression? Deserialize(string json);
}
  • The serialiser class uses US spelling. The reason for this is to maintain consistency with the System.Text.Json library and to adhere with the default IDE spellcheck suggestions.
  • The Deserialize method returns a nullable Expression type. This is because some strings do not represent a valid expression. Expression-convertible strings get mapped to their corresponding Expression objects, while expression-nonconvertible strings get mapped to null.

The serialiser requires validation of its correctness. One way to validate it is through unit tests. One way to set up unit tests is the following three-step process:

  1. Create one expression by deserialisation.
  2. Create another expression by using static builder methods that the Expression class already implements.
  3. Compare that the two expressions are equal.

The last step requires the Expression class to implement equality comparison.

This leads to a three-step plan: implement equality comparison, set up tests, implement the serialiser.

Implementation

This section details the commit-by-commit implementation in 5 steps.

Step 0: Branching

This step sets up a dedicated git branch for developing this solution. Assuming that the current branch is main, the command git pull makes the local code up to date with remote. And the command git checkokut -b feature/json creates a new branch named feature/json and switches to it.

Step 1: Value object

The two types of equality comparisons in C# are reference equality and value equality.

  • Reference equality checks whether two objects point to the same location in memory. It is the default equality that C# implements for all reference types.
  • Value equality checks whether two objects store the same values.

This solution requires value equality because expressions should be considered equal regardless of the way they are built.

One way to implement value equality is to have an abstract value object class that implements comparison logic and have derived classes pass on a stream of their custom equality components.

This commit borrows the value objects implementation from here. And the next commit customises the implementation to this solution.

Step 2: Operations comparison

This step implements value equality for operations. It does so in two parts. The first commit sets up the tests to check value equality comparison for operations. At this point half of the tests are failing. The next commit provides implementation that passes all the tests.

There are two key points.

  • The Operation class does not have any equality components.Nonetheless, different subclasses are correctly compared as non-equal because the ValueObject class also compares the object types.
  • The Constant class gets the base equality components in addition to its own. In this case including it is unnecessary because the Operation class does not have any equality components; however, it is necessary in general.

Step 3: Expressions comparison

This step implements value equality for expressions. Two expressions are equal when they have the same operation and their subexpressions are equal.

The implementation proceeds in the same way as for operations: the first commit sets up the tests, the second commit provides implementation, and the third commit does some refactoring.

Step 4: Serialiser

This step implements the serialiser.

The first commit sets up the interface and tests. Since no implementation is provided yet, all tests are failing. It also includes the following class that acts as an intermediate model between json strings and expressions:

private sealed class ExpressionModel
{
	public double? Value { get; set; }
	public string? Operation { get; set; }
	public List<ExpressionModel>? Subexpressions { get; set; }
}

The second commit implements deserialisation.

The third commit implements serialisation.

There are 4 key points.

  • The serialiser uses System.Text.Json library to convert between ExpressionModel and json strings.
  • The Operation class enforces all subclasses to have a name by using public abstract string property Name.
  • The Operation class implements a static builder method FromName to create operations by name.
  • The Expression class exposes its private fields as readonly public properties.

Conclusion

Expressions can now be compared to each other and converted to and from JSON.

JSON stands for ...JavaScript Object Notation. Reference: https://en.wikipedia.org/wiki/JSON
The two types of equality comparisons in C# are ...Reference equality & Value equality. Reference: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/equality-comparisons
Git CLI command to create a new branch X and switch to it is ...git checkout -b X. Reference: https://www.git-scm.com/docs/git-checkout#Documentation/git-checkout.txt-emgitcheckoutem-b-Bltnew-branchgtltstart-pointgt
The way that this solution implements value equality is ...Deriving from ValueObject class and overriding GetEqualityComponents method.
The serialisation library that this solution uses is ...System.Text.Json. Reference: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview
JsonSerializerOptions to ignore nulls is ...DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull. Reference: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/ignore-properties#ignore-all-null-value-properties

Read more