Search code examples
c#.net-corejson-deserializationsystem.text.jsonblazor-jsinterop

Deserializing JSON to an object - Whole object not being deserialized


I am receiving and object from a JS calendar via JSInterop in Blazor. I have a C# interface and class that represents the JS object. A few properties are not being deserialized. In Particular the End Property.

Deserialize like so:

var currentEvent = JsonSerializer.Deserialize<TUIEvent>(eventBeingModified.ToString(), new JsonSerializerOptions() { PropertyNameCaseInsensitive=true});

The incoming JSON:

{
  "id": "c7d1fa58-af2d-4b7b-8040-e9c92ad61574",
  "calendarId": "1",
  "__cid": 43,
  "title": "Voluptas molestiae consectetur enim exercitationem reprehenderit error.",
  "body": "Aliquid iste dignissimos provident sit occaecati. Distinctio itaque maxime cupiditate quae nihil dolorem deleniti aliquid. Qui voluptas et qui.",
  "isAllday": false,
  "start": {
    "tzOffset": -240,
    "d": {
      "d": "2024-03-22T01:30:00.000Z"
    }
  },
  "end": {
    "tzOffset": -240,
    "d": {
      "d": "2024-03-22T04:10:00.000Z"
    }
  },
  "goingDuration": null,
  "comingDuration": null,
  "location": null,
  "attendees": null,
  "category": "time",
  "dueDateClass": "",
  "recurrenceRule": null,
  "state": 0,
  "isVisible": true,
  "isPending": false,
  "isFocused": false,
  "isReadOnly": false,
  "isPrivate": false,
  "color": null,
  "backgroundColor": null,
  "dragBackgroundColor": null,
  "borderColor": null,
  "customStyle": null,
  "raw": null
}

The C# Class:

[JsonConverter(typeof(JsonStringEnumMemberConverter))]
public enum EventState
{
    [JsonPropertyName("busy")] Busy,
    [JsonPropertyName("free")] Free
}

[JsonConverter(typeof(JsonStringEnumMemberConverter))]
public enum EventCategory
{
    [JsonPropertyName("milestone")] Milestone,
    [JsonPropertyName("task")] Task,
    [JsonPropertyName("allday")] Allday,
    /// <summary>
    /// 
    /// </summary>
    [JsonPropertyName("time")] Time
}


/// <summary>
/// https://nhn.github.io/tui.calendar/latest/EventObject
/// </summary>
public class TUIEvent : IEventObject
{
    public string Id { get; set; }
    public string CalendarId { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
    public bool IsAllDay { get; set; }
    
    [JsonConverter(typeof(TZDateJsonConverter))]
    public DateTimeOffset? Start { get; set; }
    
    [JsonConverter(typeof(TZDateJsonConverter))]
    public DateTimeOffset? End { get; set; }
    
    public int? GoingDuration { get; set; }
    
    public int? ComingDuration { get; set; }
    
    public string Location { get; set; }
    
    public string[] Attendees { get; set; }

    public EventCategory Category { get; set; }
    
    public string RecurrenceRule { get; set; }
    
    public EventState State { get; set; }
    public bool IsVisible { get; set; }
    public bool IsPending { get; set; }
    public bool IsFocused { get; set; }
    public bool IsReadOnly { get; set; }
    public bool IsPrivate { get; set; }
    public string Color { get; set; }
    public string BackgroundColor { get; set; }
    public string DragBackgroundColor { get; set; }
    public string BorderColor { get; set; }
    public string CustomStyle { get; set; }
    public string Raw { get; set; }
}

The TZDateConvertor Read Method:

private const string TZDateFormat = @"yyyy-MM-ddTHH:mm:ss.fffZ";

static byte[] _date = Encoding.UTF8.GetBytes("d"); //"_date"

public override DateTimeOffset Read(ref Utf8JsonReader reader, 
                                    Type typeToConvert, 
                                    JsonSerializerOptions options)
{
    switch (reader.TokenType)
    {
        case JsonTokenType.String:
            // A simple DateTimeOffset string "value"
            return DateTimeOffset.ParseExact(reader.GetString(), TZDateFormat, CultureInfo.InvariantCulture);
        case JsonTokenType.StartObject:
            {
                // A DateTimeOffset string embedded in an object { "_date" : "value" }
                DateTimeOffset? value = null;
                while (reader.Read())
                {
                    switch (reader.TokenType)
                    {
                        case JsonTokenType.EndObject:
                            return value.GetValueOrDefault();
                        case JsonTokenType.PropertyName:
                            var match = reader.ValueTextEquals(_date);

                            if (match)
                            { 
                                reader.Read();
                                while (reader.TokenType != JsonTokenType.PropertyName) { reader.Read(); }
                                reader.Read();
                                var propValue = reader.GetString();
                                value = DateTimeOffset.ParseExact(propValue, TZDateFormat, CultureInfo.InvariantCulture);
                            }
                            else
                                reader.Skip();
                            break;
                        default:
                            throw new JsonException();
                    }
                }
            }
            break;
    }
    throw new JsonException();
}

And example of serialized TUIEvent object(turned back to JSON for read-ability:

{
  "Id": "7ef281a6-3086-479f-a630-9967e7449db6",
  "CalendarId": "1",
  "Title": "Magnam consequatur enim consequatur maiores non dolor.",
  "Body": "Eaque fuga tempore ab inventore porro iusto. Similique vitae ducimus voluptatem neque adipisci occaecati accusamus. Adipisci ipsum dolore laudantium. Et et quas quisquam velit adipisci et est.",
  "IsAllDay": false,
  "Start": "2024-03-10T03:00:00+00:00",
  "End": null,
  "GoingDuration": null,
  "ComingDuration": null,
  "Location": null,
  "Attendees": null,
  "Category": "milestone",
  "RecurrenceRule": null,
  "State": "busy",
  "IsVisible": false,
  "IsPending": false,
  "IsFocused": false,
  "IsReadOnly": false,
  "IsPrivate": false,
  "Color": null,
  "BackgroundColor": null,
  "DragBackgroundColor": null,
  "BorderColor": null,
  "CustomStyle": null,
  "Raw": null
}

Why is End,IsVisible, and likely others not being parsed/deserialized into the object??


Solution

  • Your problem is that the UtfJsonReader is not positioned correctly at the end of your Read() method. It should be positioned at the end of the outer object like so:

    {
       "tzOffset":-240,
       "d":{
          "d":"2024-03-22T01:30:00.000Z"
       }
    } // <== Here
    

    But you are actually leaving it positioned at the end of the inner object:

    {
       "tzOffset":-240,
       "d":{
          "d":"2024-03-22T01:30:00.000Z"
       }  // <== Here
    }
    

    You can confirm this by checking Utf8JsonReader.CurrentDepth as shown in demo fiddle #1 here. And for a related question, see System.Text.Json deserialization fails with JsonException "read to much or not enough".

    As a solution, I would recommend to simplify your code by first deserializing to some appropriate DTO, then mapping the DTO to the final DateTimeOffset like so:

    public class TZDateJsonConverter : JsonConverter<DateTimeOffset>
    {
        private const string TZDateFormat = @"yyyy-MM-ddTHH:mm:ss.fffZ";
        record Dto(decimal? tzOffset, ValueDto d) { public (decimal? tzOffset, string? d) AsTuple() => (tzOffset, d?.d); }
        record ValueDto(string d);
    
        public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            (var tzOffset, var d) = reader.TokenType switch
            {
                JsonTokenType.String => (default(decimal?), reader.GetString()),
                JsonTokenType.StartObject => JsonSerializer.Deserialize<Dto>(ref reader, options)!.AsTuple(),
                JsonTokenType.Null => default, // Remove if you don't want to allow a null value.
                _ => throw new JsonException(),
            };
            
            var value = d != null ? DateTimeOffset.ParseExact(d, TZDateFormat, CultureInfo.InvariantCulture) : default;
            // Handle tzOffset somehow?
            return value;
        }
        
        public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
            throw new NotImplementedException();
    }
    

    Not only does this approach guarantee that the reader will be correctly positioned, it also results in code that is much simpler.

    Demo fiddle #2 here.