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??
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.