1

I'm trying to deserialize a snippet of JSON that looks something like this:

{
    "timePoints": [ "2020", "2020-1-9", "2020-4-1" ]
}

The idea here is that we will allow either a fully qualified date, or just a year which will be converted to a nominal date. So I created a custom JsonConverter to handle this field:

public class Foo
{
    [JsonConverter(typeof(TimePointConverter))]
    public IEnumerable<DateTime> TimePoints { get; set; }
}

public class TimePointConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        reader.DateParseHandling = DateParseHandling.None;

        var obj = JArray.ReadFrom(reader);
        var points = obj.Select(x =>
        {
            if (!DateTime.TryParse(x.ToString(), out var date))
            {
                if (int.TryParse(x.ToString(), out int year) && year > 0)
                {
                    date = new DateTime(year, 10, 1);   // qtr 4
                }
                else
                {
                    throw new FormatException($"Unable to parse date: {x.ToString()} is invalid.");
                }
            }
            return date;
        });
        return points.Any() ? points.ToArray() : null;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

But the problem is that JArray.ReadFrom will automatically try to interpret 2020-1-9 and 2020-4-1 as dates. But since 2020 isn't a date, it just ignores it altogether. So I end up with obj being an array of two dates silently ignore the third item. How do I stop this behavior? I want it to read these as strings, not as dates? DateParseHandling.None seems to have no effect at all.

Update: Turned out I was looking in the wrong place. This was being serialized somewhere else before it even got to the application I was looking at and that was removing the non-date item. Closing this question now...

Matt Burland
  • 44,552
  • 18
  • 99
  • 171
  • Cannot replicate with the above code, it seems to read correctly – Charlieface Feb 10 '22 at 22:20
  • This is a date converter I had to write that should get you going in the right direction: [TheMovieDbWrapper - IsoDateTimeConverter](https://github.com/nCubed/TheMovieDbWrapper/blob/master/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs) – Metro Smurf Feb 10 '22 at 23:59
  • Probably, in `ReadJson()`, you need to load initially with `DateParseHandling.None`. See e.g. [JToken: Get raw/original JSON value](https://stackoverflow.com/q/35138346/3744182) and/or [How to prevent a single object property from being converted to a DateTime when it is a string](https://stackoverflow.com/q/40632820/3744182). – dbc Feb 11 '22 at 05:09

1 Answers1

1

This works for me

Foo.cs

public sealed record Foo(IReadOnlyCollection<DateTime> TimePoints);

Main method

    var json = @"
{
    ""timePoints"": [ ""2020"", ""2020-1-9"", ""2020-4-1"" ]
}";

    var settings = new JsonSerializerSettings
    {
        Converters = new List<JsonConverter> { new TimePointConverter() }
    };

    var foo = JsonConvert.DeserializeObject<Foo>(json, settings);

Converter

public class TimePointConverter : JsonConverter
{
    public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        reader.DateParseHandling = DateParseHandling.None;

        var obj = JArray.ReadFrom(reader);
        // select 'timePoints' element from the json
        var points = obj["timePoints"]?.Select(x =>
        {
            if (!DateTime.TryParse(x.ToString(), out var date))
            {
                if (int.TryParse(x.ToString(), out int year) && year > 0)
                {
                    date = new DateTime(year, 10, 1);   // qtr 4
                }
                else
                {
                    throw new FormatException($"Unable to parse date: {x.ToString()} is invalid.");
                }
            }
            return date;
        });
        
        // returns a Foo rather than list of DateTimes
        return points is not null && points.Any() 
            ? new Foo(points.ToArray())
            : null;
    }

    public override bool CanConvert(Type objectType) 
        => true;

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 
        => throw new NotImplementedException();
}

But I assumed you are calling DeserializeObject<Foo>

Bassie
  • 9,529
  • 8
  • 68
  • 159