Home > other >  How do I get the property path from Utf8JsonReader?
How do I get the property path from Utf8JsonReader?

Time:09-16

With NewtonSoft, we could get the path with reader.Path. System.Text.Json does not have this.

namespace API.JsonConverters
{
    using System;
    using System.Text.Json;
    using System.Text.Json.Serialization;

    /// <summary>
    /// Use DateTime.Parse to replicate how Newtonsoft worked.
    /// </summary>
    /// <remarks>https://docs.microsoft.com/en-us/dotnet/standard/datetime/system-text-json-support</remarks>
    public class DateTimeConverterUsingDateTimeParse : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            try
            {
                return DateTime.Parse(reader.GetString(), styles: System.Globalization.DateTimeStyles.RoundtripKind);
            }
            catch (FormatException)
            {
                // We have to do this to have the Path variable auto populated so when the middleware catches the error, it will properly populate the ModelState errors.
                // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#error-handling
                // https://github.com/dotnet/aspnetcore/blob/release/3.1/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs#L79
                throw new JsonException("Invalid DateTime. Please use RoundTripKind (MM-DD-YYYY) - https://docs.microsoft.com/en-us/dotnet/standard/base-types/how-to-round-trip-date-and-time-values");
            }
        }

        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString("o"));
        }
    }
}

How can I get access to the current path so that I can throw an exception that has both a custom message and a Path from within the Read() method of my custom JsonConverter?

CodePudding user response:

Within a JsonConverter, you want to throw a custom exception with a custom message and also include JSONPath information. As explained by the docs, System.Text.Json only appends path information to an exception of type JsonException - and only when the exception has no message. So, how can the path information be included?

The obvious way to do this would be to get the current path from within JsonConverter<T>.Read() and pass it to your exception's constructor. Unfortunately, System.Text.Json does not make the path available to Read() or Write(). This can be confirmed by checking the reference source. Utf8JsonReader currently does not even know the path. All it knows is the stack of container types (object or array) using a BitStack member Utf8JsonReader._bitStack, which is the minimum necessary to handle its state transitions correctly when exiting a nested container. JsonSerializer does track the current stack, via the ReadStack ref struct which has a JsonPath() method. Unfortunately ReadStack is internal and never exposed to applications or to Utf8JsonReader.

As a workaround, you could create nested exceptions with the inner exception being your required exception type and the outer exception being a JsonException into which the serializer will populate the path. The following is one example of how to do this:

public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    try
    {
        return DateTime.Parse(reader.GetString(), 
                              styles: System.Globalization.DateTimeStyles.RoundtripKind); 
    }
    catch (FormatException)
    {
        var innerEx = new ProblemDetailsException("Invalid DateTime. Please use RoundTripKind (MM-DD-YYYY) - https://docs.microsoft.com/en-us/dotnet/standard/base-types/how-to-round-trip-date-and-time-values");
        throw new JsonException(null, innerEx);
    }
}

Then at a higher level you could catch the JsonException and throw an outer ProblemDetailsException, e.g. like so:

public class JsonExtensions
{
    public static T Deserialize<T>(string json, JsonSerializerOptions options = default)
    {
        try
        {
            return JsonSerializer.Deserialize<T>(json, options);
        }
        catch (JsonException ex) when (ex.InnerException is ProblemDetailsException innerException)
        {
            var finalException = new ProblemDetailsException(innerException.Message, ex.Path, ex);
            throw finalException;
        }
    }
}

Notes:

  • Here I as assuming ProblemDetailsException looks something like:

    public class ProblemDetailsException : System.Exception
    {
        public ProblemDetailsException(string message) : base(message) { }
        public ProblemDetailsException(string message, Exception inner) : base(message, inner) { }
        public ProblemDetailsException(string message, string path) : base(message) => this.Path = path;
        public ProblemDetailsException(string message, string path, Exception inner) : base(message, inner) => this.Path = path;
    
        public string Path { get; }
    }
    
  • You might consider parsing your DateTime using CultureInfo.InvariantCulture:

    return DateTime.Parse(reader.GetString(), 
                          styles: System.Globalization.DateTimeStyles.RoundtripKind, 
                          provider: System.Globalization.CultureInfo.InvariantCulture);
    

    As written currently, your converter will function differently in different locales. Or, if you really want to parse in the current locale, make that explicit in your code:

    return DateTime.Parse(reader.GetString(), 
                          styles: System.Globalization.DateTimeStyles.RoundtripKind, 
                          provider: System.Globalization.CultureInfo.CurrentCulture);
    

Demo fiddle here.

  • Related