Reputation: 5871
I have a .NET 6 solution for which I'm trying to override the default format of DateTimeOffset
's when calling JsonObject.ToJsonString()
. This is all using the native System.Text.Json
libraries.
I've added a custom DateTimeOffsetConverter
:
public class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
private readonly string _format;
public DateTimeOffsetConverter(string format)
{
_format = format;
}
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Assert(typeToConvert == typeof(DateTimeOffset));
return DateTimeOffset.Parse(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(_format));
}
public override bool CanConvert(Type typeToConvert)
{
return (typeToConvert == typeof(DateTimeOffset));
}
}
But when I try to use it, the code is never hit beyond the constructor being called.
What am I missing that's preventing the JsonConverter
being called?
Here's my code which tries to make use of the functionality:
[Theory]
[InlineData("New Zealand Standard Time")]
[InlineData("India Standard Time")]
[InlineData("Central Brazilian Standard Time")]
[InlineData("W. Australia Standard Time")]
public void DateTimeOffsetIsSerializedCorrectlyTest(string timeZoneId)
{
const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffzzz";
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
var dateTimeOffset = new DateTimeOffset(DateTimeOffset.Now.DateTime, timeZoneInfo.BaseUtcOffset);
var json = new JsonObject
{
{ "value", dateTimeOffset }
};
var options = new JsonSerializerOptions
{
Converters = { new DateTimeOffsetConverter(DateTimeFormat) }
};
string jsonString = json.ToJsonString(options);
Assert.Contains(jsonString, dateTimeOffset.ToString(DateTimeFormat));
}
There's a number of closely related question already posted, who's solutions I've experimented with, but none seem to address my precise scenario.
Upvotes: 1
Views: 1260
Reputation: 116585
It looks as though the converter is applied at the time the DateTimeOffset
is converted to a JsonNode
rather than when the JsonNode
is formatted to a string. Thus you may generate the required JSON by explicitly serializing your DateTimeOffset
rather than relying on implicit conversion:
var options = new JsonSerializerOptions
{
Converters = { new DateTimeOffsetConverter(DateTimeFormat) }
};
var json = new JsonObject
{
{ "value", JsonSerializer.SerializeToNode(dateTimeOffset, options) }
};
This results in {"value":"2022-11-27T15:10:23.570\u002B12:00"}
. Note that the +
is escaped. If your require it not to be escaped so that your Assert.Contains(jsonString, dateTimeOffset.ToString(DateTimeFormat));
passes successfully, use UnsafeRelaxedJsonEscaping
:
var formattingOptions = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
// Other formatting options as required, e.g.
//WriteIndented = true, // This option also seems to be applied when the JsonNode is formatted to a string, rather than when it is constructed
};
string jsonString = json.ToJsonString(formattingOptions);
Which results in {"value":"2022-11-27T15:17:44.142+12:00"}
.
Demo fiddle #1 here.
Notes:
The documentation page How to use a JSON document, Utf8JsonReader, and Utf8JsonWriter in System.Text.Json: JsonNode
with JsonSerializerOptions
states:
You can use
JsonSerializer
to serialize and deserialize an instance ofJsonNode
. However, if you use an overload that takesJsonSerializerOptions
, the options instance is only used to get custom converters. Other features of the options instance are not used. ...
This statement seems to be incorrect; your DateTimeOffsetConverter
is not picked up even if you serialize your JsonNode
hierarchy with JsonSerializer.Serialize(json, options)
.
Demo fiddle #2 here.
I can't find any documentation listing the options applied during JsonNode
construction vs JsonNode
string formatting. The docs do show that WriteIndented
is applied during string formatting. Some experimentation shows that, in addition, Encoder
and (surprisingly) NumberHandling
are applied during string formatting.
Demo fiddle #3 here.
Some debugging shows why your DateTimeOffsetConverter
is not applied during string formatting. The DateTimeOffset
to JsonNode
implicit conversion operator:
JsonNode node = dateTimeOffset;
Returns an object of type JsonValueTrimmable<DateTimeOffset>
. This type includes its own internal converter:
internal sealed partial class JsonValueTrimmable<TValue> : JsonValue<TValue>
{
private readonly JsonTypeInfo<TValue>? _jsonTypeInfo;
private readonly JsonConverter<TValue>? _converter;
If we access the value of _converter
with reflection, we find it is initialized to the system converter DateTimeOffsetConverter
:
var fi = node.GetType().GetField("_converter", BindingFlags.NonPublic | BindingFlags.Instance);
Console.WriteLine("JsonValueTrimmable<T>._converter = {0}", fi.GetValue(node)); // Prints System.Text.Json.Serialization.Converters.DateTimeOffsetConverter
This system converter ignores the incoming options and simply calls Utf8JsonWriter.WriteStringValue(DateTimeOffset);
.
Demo fiddle #4 here.
Upvotes: 2