I'm trying to use NodaTime to interpret dates retrieved from a third-party API. The dates come in an annoying array of formats in the same response, one that I'm particularly having trouble with is similar to:
{
"ShortDate": "2017-01-01",
"LongDate": "01 January 2017"
}
I can correctly deserialise one format or the other using NodaPatternConverter, but not both.
A simple example showing the problem is like this:
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using NodaTime.Text;
namespace NodaLocalDateConverterTest
{
class ExampleDatedModel
{
public LocalDate ShortDate { get; set; }
public LocalDate LongDate { get; set; }
}
class Program
{
static void Main(string[] args)
{
var exampleJsonString =
@"{
""ShortDate"": ""2017-01-01"",
""LongDate"": ""01 January 2017""
}";
var serialisationSettings = new JsonSerializerSettings();
//NodaTime default converter supports ShortDate format
serialisationSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
//Exception on LongDate property
var deserialisedExample1 = JsonConvert.DeserializeObject<ExampleDatedModel>(exampleJsonString, serialisationSettings);
serialisationSettings.Converters.Remove(NodaConverters.LocalDateConverter);
serialisationSettings.Converters.Add(new NodaPatternConverter<LocalDate>(LocalDatePattern.CreateWithInvariantCulture("dd MMMM yyyy")));
//Exception on ShortDate property
var deserialisedExample2 = JsonConvert.DeserializeObject<ExampleDatedModel>(exampleJsonString, serialisationSettings);
}
}
}
Using the default serialiser gives throws an exception on the LongDate property:
An unhandled exception of type 'NodaTime.Text.UnparsableValueException' occurred in Newtonsoft.Json.dll
Additional information: The value string does not match the required number from the format string "yyyy". Value being parsed: '^01 January 2017'. (^ indicates error position.)
Replacing with a custom pattern converter throws an exception on the ShortDate property:
An unhandled exception of type 'NodaTime.Text.UnparsableValueException' occurred in Newtonsoft.Json.dll
Additional information: The value string does not match a simple character in the format string " ". Value being parsed: '20^17-01-01'. (^ indicates error position.)
In principle I think I could use two different converters for the two properties, such as
class ExampleDatedModel
{
[JsonConverter(typeof(ShortDateConverter)]
public LocalDate ShortDate { get; set; }
[JsonConverter(typeof(LongDateConverter)]
public LocalDate LongDate { get; set; }
}
However I don't see how to use NodaTime's NodaPatternConverter with the attribute as you can't instantiate the converter with a pattern.
The documentation helpfully says "Custom converters can be created easily from patterns using NodaPatternConverter." but doesn't give any examples!
Possible solutions I've though about are
But I hope I'm just missing a way of marking up the resource classes to use existing converters.
This does indeed seem to be a use case we hadn't considered. For "normal" usage, sealing NodaPatternConverter
feels like the right approach - but when a JsonConverter
has to be specified by type rather than instantiated, the sealing is frustrating. I've filed an issue to fix this in 2.0, which I'm hoping to release in the next month or so. (It's now implemented - the pull request shows sample usage as well.)
However, for the meantime I probably would just fork NodaPatternConverter
- and add a comment saying that it's only there until you can use 2.0.
You may want to trim it down a bit, as you probably don't need the extra validation, assuming you control all the code that's going to serialize data - if you don't need to worry about non-ISO LocalDate
values, you probably don't need validation.
The other aspect is that if you're only ever parsing using the converters, you don't need the writing side at all - you could just throw an exception for that at the moment, potentially.
An alternative to unsealing NodaPatternConverter
is to have a simple (abstract) DelegatingConverterBase
type, which delegates to another JsonConverter
. Typical usage would be something like:
public sealed class ShortDateConverter : DelegatingConverterBase
{
public ShortDateConverter() : base(NodaConverters.LocalDate) {}
}
That might be a more elegant separation of concerns - and implementable with less code until it's part of Noda Time, too:
public abstract class DelegatingConverterBase : JsonConverter
{
private readonly JsonConverter original;
protected DelegatingConverterBase(JsonConverter original)
{
this.original = original;
}
public override void WriteJson(
JsonWriter writer, object value, JsonSerializer serializer) =>
original.WriteJson(writer, value, serializer);
public override object ReadJson(
JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) =>
original.ReadJson(reader, objectType, existingValue, serializer);
public override bool CanRead => original.CanRead;
public override bool CanWrite => original.CanWrite;
public override bool CanConvert(Type objectType) => original.CanConvert(objectType);
}
See more on this question at Stackoverflow