0

I am trying to deserialize json data with System.Text.Json utilizing converters.

  • The raw content of the http response shows that the json contains valid data
  • The converter to deserialize the content into the type specified observable collection is being called and produces the correct collection with all data except the TIMESPAN.
  • The TimeSpan converter is only being called if it is NOT a collection, only if it is a single object.
  • The problem seems to be something with "converter" needs another "converter" for a nested object.

Is there any help or experience on this matter out there?

The ObservableCollectionJsonConverter

public class ObservableCollectionJsonConverter<T> : JsonConverter<ObservableCollection<T>> where T : class
{
    public override ObservableCollection<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        ObservableCollection<T> collection = null;
        var startDepth = reader.CurrentDepth;
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth) return collection;
            if (reader.TokenType == JsonTokenType.StartArray)
            {
                var deserialized = JsonSerializer.Deserialize<T[]>(ref reader, options);
                collection = new ObservableCollection<T>(deserialized);
            }
        }
        return collection;
    }

    public override void Write(Utf8JsonWriter writer, ObservableCollection<T> value, JsonSerializerOptions options) => writer.WriteStringValue(JsonSerializer.Serialize(value));
}

The TimeSpanJsonConverter

public class TimeSpanJsonConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        long ticks = 0;
        var startDepth = reader.CurrentDepth;
        if (reader.TokenType == JsonTokenType.StartObject)
        {
            string propertyName = null;
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.EndObject when reader.CurrentDepth == startDepth:
                        return TimeSpan.FromTicks(ticks);
                    case JsonTokenType.PropertyName:
                        propertyName = reader.GetString();
                        break;
                }
                if (!string.IsNullOrWhiteSpace(propertyName) &&
                    propertyName.Equals("Ticks") &&
                    reader.TokenType == JsonTokenType.Number) ticks = reader.GetInt64();
            }
        }
        return TimeSpan.Zero;
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) => writer.WriteStringValue(JsonSerializer.Serialize(value));
}

The object to be deserialized

public class Incident : ModelBase
{
    private string _uniqueId = default;
    private int _completion = default;
    private Status _status = default;
    private TimeSpan _estimated = default;
    private TimeSpan _actual = default;
    private DateTime _closed = default;
    private string _comments = default;
    private DateTime _opened = default;
    private DateTime _updated = default;
    private string _briefDescripion = default;
    private Project _project = default;

    /// <summary>
    /// The name of the project
    /// </summary>
    [JsonPropertyName("project")]
    public Project Project { get => _project; set => SetProperty(ref _project, value); }
    /// <summary>
    /// The incident's unique id
    /// </summary>
    [JsonPropertyName("uniqueId")]
    public string UniqueId { get => _uniqueId; set => SetProperty(ref _uniqueId, value); }
    /// <summary>
    /// The level of completion
    /// </summary>
    [JsonPropertyName("completion")]
    public int Completion { get => _completion; set => SetProperty(ref _completion, value); }
    /// <summary>
    /// The incident's state
    /// </summary>
    [JsonPropertyName("status")]
    public Status Status { get => _status; set => SetProperty(ref _status, value); }
    /// <summary>
    /// The expected time to resolve the incident
    /// </summary>
    [JsonPropertyName("estimated")]
    public TimeSpan Estimated { get => _estimated; set => SetProperty(ref _estimated, value); }
    /// <summary>
    /// The actual time on the incident
    /// </summary>
    [JsonPropertyName("actual")]
    public TimeSpan Actual { get => _actual; set => SetProperty(ref _actual, value); }
    /// <summary>
    /// The time when the incident was opened
    /// </summary>
    [JsonPropertyName("opened")]
    public DateTime Opened { get => _opened; set => SetProperty(ref _opened, value); }
    /// <summary>
    /// The time when the incident has been last updated
    /// </summary>
    [JsonPropertyName("updated")]
    public DateTime Updated { get => _updated; set => SetProperty(ref _updated, value); }
    /// <summary>
    /// The time when the incident has been closed
    /// </summary>
    [JsonPropertyName("closed")]
    public DateTime Closed { get => _closed; set => SetProperty(ref _closed, value); }
    /// <summary>
    /// The collection of reports assigned to the incident
    /// </summary>        
    [JsonPropertyName("reports")]
    [JsonInclude]
    public virtual ICollection<Report> Reports { get; set; } = new HashSet<Report>();
    /// <summary>
    /// The customer the incident is assigned to
    /// </summary>
    [JsonPropertyName("customer")]
    public virtual Customer Customer { get; set; }
    /// <summary>
    /// The supporter the incident is assigned to
    /// </summary>
    [JsonPropertyName("supporter")]
    public virtual Supporter Supporter { get; set; }
    /// <summary>
    /// Comments to document the case
    /// </summary>
    [JsonPropertyName("comments")]
    public string Comments { get => _comments; set => SetProperty(ref _comments, value); }
    /// <summary>
    /// Brief description about the incident
    /// </summary>
    [JsonPropertyName("briefDescripion")]
    public string BriefDescripion { get => _briefDescripion; set => SetProperty(ref _briefDescripion, value); }
}
Marcus Runge
  • 635
  • 6
  • 17
  • Can you please [edit] your question to share a [mcve]? 1) The JSON is not included in your question; 2) your `Incident` model does not compile due to lack of definitions for other classes, see https://dotnetfiddle.net/oIM2Nd; 3) The call to `JsonSerializer.Deserialize()` and the serialization options used is not shown. – dbc Feb 21 '21 at 15:05
  • In your converters you may be reading too much or too little or your data model might not match your JSON. Hard to say without a [mcve]. Incidentally your `Incident` model doesn't even include an `ObservableCollection`. – dbc Feb 21 '21 at 15:08
  • Using a made-up data model I only found a couple of problems with your converters. 1) `ObservableCollectionJsonConverter` reads too much when the start token is an array. 2) Both converters use the wrong technique to generate a default serialization in the `Write()` method. To do that see [How to use default serialization in a custom System.Text.Json JsonConverter?](https://stackoverflow.com/q/65430420/3744182). Demo fiddle [here](https://dotnetfiddle.net/tVKD9k). We need to see a [mcve] to answer your question any further. – dbc Feb 21 '21 at 18:55
  • If you are working in .Net 3.0 then maybe this is the issue: [`[System.Text.Json]` JsonSerializer ignores MaxDepth option #882](https://github.com/dotnet/runtime/issues/882): *This bug is not just specific to converters (nor just to MaxDepth) and will be hit whenever the caller passes in a Utf8JsonReader to the JsonSerializer.Deserialize method with non-default JsonReaderOptions set... Here's the issue (**note the new Utf8JsonReader is created without passing in the user defined options**)... We should consider fixing this in 3.1 as well.* – dbc Feb 21 '21 at 19:19
  • Actually It's Net 5.0 – Marcus Runge Feb 22 '21 at 17:08

1 Answers1

0

For some reason, "little" changes to the timespan converter did the job:

public class TimeSpanJsonConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        long ticks = 0;
        var startDepth = reader.CurrentDepth;
        if (reader.TokenType == JsonTokenType.StartObject)
        {
            string propertyName = null;
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.EndObject when reader.CurrentDepth == startDepth:
                        return TimeSpan.FromTicks(ticks);
                    case JsonTokenType.PropertyName:
                        propertyName = reader.GetString();
                        break;
                }
                if (!string.IsNullOrWhiteSpace(propertyName) && propertyName.Equals("Ticks") && reader.TokenType == JsonTokenType.Number) ticks = reader.GetInt64();
            }
        }
        else if (reader.TokenType == JsonTokenType.Number) return TimeSpan.FromTicks(reader.GetInt64());
        return TimeSpan.Zero;
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) => writer.WriteNumberValue(value.Ticks);
}
Marcus Runge
  • 635
  • 6
  • 17