I have a class that represents an expression tree with three properties: Left
containing either another expression or a discrete value, Operator
containing an enumeration of operation types, and Right
which can contain another expression or a discrete value.
public class Expr
{
public object Left { get; set; }
public OperatorEnum Operator { get; set; }
public object Right { get; set; }
}
When I deserialize this using System.Text.Json
the resulting instance Left
and Right
properties are always of type JsonElement
whether they contain a nested Expr
or a discrete value (string
, decimal
, etc).
i.e. when I deserialize { "Left": "foo", "Operator": "Equals", "Right": "bar" }
into instance Expr expr
, expr.Left.GetType()
always returns JsonElement
.
I’ve built a JsonConverter
class and am adding it to the JsonSerializerOptions
for both serialization and deserialization, but admittedly, I don’t have a lot of experience with it.
namespace Test
{
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
public class ExpressionConverter : JsonConverter<Expr>
{
public override Expr Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Expr ret = new Expr();
bool inLeft = false;
bool inOper = false;
bool inRight = false;
while (reader.Read())
{
if (!inLeft && !inOper && !inRight)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
if (reader.ValueTextEquals("Left"))
{
inLeft = true;
}
else if (reader.ValueTextEquals("Operator"))
{
inOper = true;
}
else if (reader.ValueTextEquals("Right"))
{
inRight = true;
}
}
}
else
{
if (inLeft)
{
if (reader.TokenType == JsonTokenType.String)
{
string val = reader.GetString();
if (val != null) ret.Left = val.ToString();
else ret.Left = null;
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
ret.Left = JsonSerializer.Deserialize<Expr>(ref reader, options);
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
ret.Left = JsonSerializer.Deserialize<List<object>>(ref reader, options);
}
else if (reader.TokenType == JsonTokenType.Number)
{
ret.Left = reader.GetDecimal();
}
else if (reader.TokenType == JsonTokenType.True)
{
ret.Left = true;
}
else if (reader.TokenType == JsonTokenType.False)
{
ret.Left = false;
}
else
{
throw new InvalidOperationException("Unexpected token type '" + reader.TokenType.ToString() + "' while processing value for 'Left' expression.");
}
}
else if (inOper)
{
ret.Operator = (OperatorEnum)(Enum.Parse(typeof(OperatorEnum), reader.GetString()));
}
else if (inRight)
{
if (reader.TokenType == JsonTokenType.String)
{
string val = reader.GetString();
if (val != null) ret.Right = val.ToString();
else ret.Left = null;
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
ret.Right = JsonSerializer.Deserialize<Expr>(ref reader, options);
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
ret.Right = JsonSerializer.Deserialize<List<object>>(ref reader, options);
}
else if (reader.TokenType == JsonTokenType.Number)
{
ret.Right = reader.GetDecimal();
}
else if (reader.TokenType == JsonTokenType.True)
{
ret.Right = true;
}
else if (reader.TokenType == JsonTokenType.False)
{
ret.Right = false;
}
else
{
throw new InvalidOperationException("Unexpected token type '" + reader.TokenType.ToString() + "' while processing value for 'Left' expression.");
}
}
inLeft = false;
inOper = false;
inRight = false;
}
}
return ret;
}
public override void Write(Utf8JsonWriter writer, Expr value, JsonSerializerOptions options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
writer.WriteStartObject();
if (value.Left is Expr)
{
writer.WritePropertyName("Left");
JsonSerializer.Serialize(writer, value.Left, options);
}
else if (IsNumberType(value.Left))
{
writer.WriteNumber("Left", Convert.ToDecimal(value.Left));
}
else if (IsBooleanType(value.Left))
{
writer.WriteBoolean("Left", Convert.ToBoolean(value.Left));
}
else
{
if (value.Left != null)
{
writer.WriteString("Left", value.Left.ToString());
}
else
{
writer.WriteNull("Left");
}
}
writer.WriteString("Operator", value.Operator.ToString());
if (value.Right is Expr)
{
writer.WritePropertyName("Right");
JsonSerializer.Serialize(writer, value.Right, options);
}
else if (IsNumberType(value.Right))
{
writer.WriteNumber("Right", Convert.ToDecimal(value.Right));
}
else if (IsBooleanType(value.Right))
{
writer.WriteBoolean("Right", Convert.ToBoolean(value.Right));
}
else
{
if (value.Right != null)
{
writer.WriteString("Right", value.Right.ToString());
}
else
{
writer.WriteNull("Right");
}
}
writer.WriteEndObject();
}
private bool IsNumberType(object value)
{
if (value == null) return false;
return Double.TryParse(value.ToString(), out double _);
}
private bool IsBooleanType(object value)
{
if (value == null) return false;
return Boolean.TryParse(value.ToString(), out bool _);
}
}
}
Is there any way to get the Left
and Right
properties to correctly reflect the value type after deserialization, e.g. Expr
, string
, etc?