I've successfully applied the JsonConverter attribute in my classes to govern the converter used for properties of objects in the past. Now, I have a dictionary with several DateTime entries which need to be converted in different formats. For example, have a look at the following dictionary:
var header = new Dictionary<string, object>
{
{"Id", reader.GetInt32(0)},
...
{"CreatedAt", reader.GetDateTime(4)},
...
{"StartDate", reader.GetDateTime(7)},
...
};
When serializing this dictionary, I wish a different format for CreatedAt and a different one for StartDate. CreatedAt should have the full ISO format, e.g. 2014-05-26T09:13:34.56 and StartDate should have only the date, e.g. 2014-05-26.
What is the preferred way of telling Json.NET how to format the entries of a dictionary?
Here is a way you can do it :
Create your own DateOnly class which encaspsulate a DateTime struct.
Implements your own type converter by extending Newtonsoft.Json.JsonConverter abstract class.
public class DateOnly
{
public DateTime Date { get; set; }
}
Conveter class
public class DateOnlyConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is DateOnly)
{
writer.WriteValue(((DateOnly) value).Date.ToString("yyyy-MM-dd"));
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string value = reader.ReadAsString();
try
{
return DateTime.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
}
catch (FormatException)
{
return null;
}
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof (DateOnly);
}
}
Add your date only class instead of DateTime struct
var header = new Dictionary<string, object>
{
{"Id", reader.GetInt32(0)},
...
{"CreatedAt", reader.GetDateTime(4)},
...
{"StartDate", new DateOnly { Date = reader.GetDateTime(7)}},
...
};
And finally
string isoJson = JsonConvert.SerializeObject(entry, new IsoDateTimeConverter(), new DateOnlyConverter ());
Related
I have a converter like this
class MultiFormatDateConverter : JsonConverter
{
public List<string> DateTimeFormats { get; set; }
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
string dateString = (string)reader.Value;
DateTime date;
foreach (string format in DateTimeFormats)
{
// adjust this as necessary to fit your needs
if (DateTime.TryParseExact(dateString, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
return date;
}
throw new System.Text.Json.JsonException("Unable to parse \"" + dateString + "\" as a date.");
}
}
and here is the configuration
var settings = new JsonSerializerSettings();
settings.DateParseHandling = DateParseHandling.None;
settings.Converters.Add(new MultiFormatDateConverter
{
DateTimeFormats = new List<string> { "yyyyMMddTHHmmssZ", "yyyy-MM-ddTHH:mm","MMMM yyyy","dd/MM/yyyy","dd/MM/yy","MMM-yy","MMM yy"
}
});
and here is how I am calling it:
List<KipReport> rpt730 = JsonConvert.DeserializeObject<List<KipReport>>(responseBody, settings);
This is the JSON and class
[
{
"Name":"Alex",
"MonthWorked":"January 2021",
"LastEdtDate":"16/02/2021",
"LastEditBy":"san"
}
]
class KipReport
{
public string Name { get; set; }
public DateTime? MonthWorked { get; set; }
public DateTime? LastEditDate { get; set; }
}
Mine is a web API and here is the controller which calls the function. Please note it calls the function as Task.Run()
[HttpGet]
public async Task<IActionResult> Get()
{
await Task.Run(()=>_kReport.GetKReports());
return Accepted();
}
When executing it says
16/03/2021 is not a valid date format
Then I used this way for converting than a converter
var settings = new IsoDateTimeConverter { DateTimeFormat = "dd/MM/yyyy" };
Then error is with January 2021 is not a valid date
Does it means, it's not considering the converter??
Since I have a different format for dates I am using a converter.
So for Web API/Task.Run do we need to do anything specific for the Custom converter?
Your properties are of type DateTime? (i.e. nullable value types) so in CanConvert you must check for objectType == typeof(DateTime?) as well as objectType == typeof(DateTime). Then, in Read(), if the incoming objectType is typeof(DateTime?) you should return null in the event of a null JSON token.
The following fixed converter does this and also skips comments:
class MultiFormatDateConverter : JsonConverter
{
public List<string> DateTimeFormats { get; set; } = new ();
public override bool CanConvert(Type objectType) =>
objectType == typeof(DateTime) || objectType == typeof(DateTime?);
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) => throw new NotImplementedException();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.MoveToContent().TokenType == JsonToken.Null)
return objectType == typeof(DateTime?) ? null : throw new System.Text.Json.JsonException("Unable to parse null as a date.");
else if (reader.TokenType != JsonToken.String)
throw new System.Text.Json.JsonException("Unable to parse token \"" + reader.TokenType + "\" as a date.");
string dateString = (string)reader.Value;
foreach (string format in DateTimeFormats)
{
// adjust this as necessary to fit your needs
if (DateTime.TryParseExact(dateString, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
return date;
}
throw new System.Text.Json.JsonException("Unable to parse \"" + dateString + "\" as a date.");
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContent(this JsonReader reader)
{
while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
;
return reader;
}
}
Notes:
In your JSON you have a property named "LastEdtDate" while the corresponding c# property is LastEditDate. The JSON property name is missing the letter i in Edit and so will not get bound to the c# property. I assume this is a typo in the question, but if not, you will need to add [JsonProperty("LastEdtDate")] to LastEditDate.
Demo fiddle here.
I'm working with an API that is returning results to me in a different way than I'm used to dealing with, and seemingly non-standard.
For example, here's a snippet of Customer data:
{
"CustomerID": {
"value": "EXAMPLE"
},
"CustomerCurrencyID": {
"value": "USD"
}
}
That "value" property seems very unnecessary, so I would like to see if I can just bypass that all together and deserialize that JSON into an object like so:
class Customer {
public string CustomerID { get; set; }
public string CustomerCurrencyID { get; set; }
}
I'm currently working on writing a custom JsonConverter to handle this, so if I'm heading down the right path just let me know, but any tips/tricks here would be much appreciated!
You can do this with a generic custom JsonConverter such as the following:
public class WrapWithValueConverter<TValue> : JsonConverter
{
// Here we take advantage of the fact that a converter applied to a property has highest precedence to avoid an infinite recursion.
class DTO { [JsonConverter(typeof(NoConverter))] public TValue value { get; set; } public object GetValue() => value; }
public override bool CanConvert(Type objectType) => typeof(TValue).IsAssignableFrom(objectType);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> serializer.Serialize(writer, new DTO { value = (TValue)value });
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> serializer.Deserialize<DTO>(reader)?.GetValue();
}
public class NoConverter : JsonConverter
{
// NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
// By https://stackoverflow.com/users/3744182/dbc
// To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
public override bool CanConvert(Type objectType) { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }
public override bool CanRead => false;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}
Then you can apply it to your model as follows:
class Customer {
[JsonConverter(typeof(WrapWithValueConverter<string>))]
public string CustomerID { get; set; }
[JsonConverter(typeof(WrapWithValueConverter<string>))]
public string CustomerCurrencyID { get; set; }
}
Demo fiddle #1 here.
Or, if you want all strings to be wrapped in a {"value": <string value>} object, you can add the converter to JsonSerializerSettings.Converters when serializing and deserializing:
var settings = new JsonSerializerSettings
{
Converters = { new WrapWithValueConverter<string>() },
};
var model = JsonConvert.DeserializeObject<Customer>(json, settings);
var json2 = JsonConvert.SerializeObject(model, Formatting.Indented, settings);
Demo fiddle #2 here.
If your value is an enum and you want to serialize it as a string, you can replace NoConverter with StringEnumConverter by using the following:
public class WrapEnumWithValueConverter<TEnum> : JsonConverter where TEnum: Enum
{
// Here we take advantage of the fact that a converter applied to a property has highest precedence to avoid an infinite recursion.
class DTO { [JsonConverter(typeof(StringEnumConverter))] public TEnum value { get; set; } public object GetValue() => value; }
public override bool CanConvert(Type objectType) => typeof(TEnum).IsAssignableFrom(objectType);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> serializer.Serialize(writer, new DTO { value = (TEnum)value });
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> serializer.Deserialize<DTO>(reader)?.GetValue();
}
Demo fiddle #3 here.
We are using .NET Core 3.1. We have a custom JSON converter for DateTime and DateTime? properties.
JsonDateTimeConverter.cs
public class JsonDateTimeConverter : DateTimeConverterBase
{
public override bool CanConvert(Type objectType)
{
// I want to return false for object properties which have attribute "TimeZoneIgnore"
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// convert datetime to timezone
DateTime? dateTime = (value as DateTime?).ConvertToTimeZone("CEST");
writer.WriteValue(dateTime);
writer.Flush();
}
}
TimeZoneIgnore.cs
[AttributeUsage(AttributeTargets.Property)]
public class TimeZoneIgnore : Attribute { }
Bank.cs
public class Bank
{
public string Name { get; set; }
public DateTime ConvertThis { get; set; }
[TimeZoneIgnore]
public DateTime DontConvertThis { get; set; }
}
TestController.cs
[HttpGet]
public IActionResult Test123()
{
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new JsonDateTimeConverter());
return Json(new Bank()
{
Name = "Test bank",
ConvertThis = new DateTime(2020, 8, 18, 15, 7, 1),
DontConvertThis = new DateTime(2020, 8, 18, 15, 7, 1)
}, settings);
}
How can I return false in CanConvert(Type objectType) for object properties with TimeZoneIgnore attribute?
A JsonConverter doesn't have the context to determine which property it is being applied to, so there is not an easy way to get the attributes from within it. On the other hand, a ContractResolver does have the context information, because it is responsible for mapping JSON properties to class properties. It turns out you can also use a ContractResolver to programmatically apply or remove JsonConverters.
So, instead of applying your JsonDateTimeConverter globally in settings, you can use a custom ContractResolver to apply it conditionally based on the presence or absence of the [TimeZoneIgnore] attribute. Here is the code you would need for the resolver:
public class ConditionalDateTimeResolver : DefaultContractResolver
{
static readonly JsonConverter DateTimeConverter = new JsonDateTimeConverter();
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty prop = base.CreateProperty(member, memberSerialization);
if (DateTimeConverter.CanConvert(prop.PropertyType) &&
!prop.AttributeProvider.GetAttributes(typeof(TimeZoneIgnore), true).Any())
{
prop.Converter = DateTimeConverter;
}
return prop;
}
}
To use the resolver, add it to the JsonSerializerSettings instead of adding your converter:
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new ConditionalDateTimeResolver();
// settings.Converters.Add(new JsonDateTimeConverter()); <-- remove this line
Here is a working demo: https://dotnetfiddle.net/8LBZ4S
I am using JsonConvert.SerializeObject to serialize a model object.
The server expects all fields as strings. My model object has numeric properties and string properties. I can not add attributes to the model object. Is there a way to serialize all property values as if they were strings? I have to support only serialization, not deserialization.
You can provide your own JsonConverter even for numeric types. I've just tried this and it works - it's quick and dirty, and you almost certainly want to extend it to support other numeric types (long, float, double, decimal etc) but it should get you going:
using System;
using System.Globalization;
using Newtonsoft.Json;
public class Model
{
public int Count { get; set; }
public string Text { get; set; }
}
internal sealed class FormatNumbersAsTextConverter : JsonConverter
{
public override bool CanRead => false;
public override bool CanWrite => true;
public override bool CanConvert(Type type) => type == typeof(int);
public override void WriteJson(
JsonWriter writer, object value, JsonSerializer serializer)
{
int number = (int) value;
writer.WriteValue(number.ToString(CultureInfo.InvariantCulture));
}
public override object ReadJson(
JsonReader reader, Type type, object existingValue, JsonSerializer serializer)
{
throw new NotSupportedException();
}
}
class Program
{
static void Main(string[] args)
{
var model = new Model { Count = 10, Text = "hello" };
var settings = new JsonSerializerSettings
{
Converters = { new FormatNumbersAsTextConverter() }
};
Console.WriteLine(JsonConvert.SerializeObject(model, settings));
}
}
I'd like DateTime fields that are set to DateTime.MinValue returned by my Web API to be serialized to NULL instead of "0001-01-01T00:00:00".
I understand there's a way to get JSON.NET to omit fields that are set to default values, but I would prefer JSON.NET to specifically serialize DateTime MinValue / "0001-01-01T00:00:00" as null.
Is there a way to do this?
Create a custom converter which serializes DateTime.MinValue into null, and (if required) deserializes null into DateTime.MinValue:
public class MinDateTimeConverter : DateTimeConverterBase
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return DateTime.MinValue;
return (DateTime)reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
DateTime dateTimeValue = (DateTime)value;
if (dateTimeValue == DateTime.MinValue)
{
writer.WriteNull();
return;
}
writer.WriteValue(value);
}
}
You can then use attributes to add the converter to your data class, as shown in this example:
public class Example
{
[JsonConverter(typeof(MinDateTimeConverter))]
public DateTime ValueOne { get; set; }
[JsonConverter(typeof(MinDateTimeConverter))]
public DateTime ValueTwo { get; set; }
}
public static void Main(string[] args)
{
Example data = new Example();
data.ValueOne = DateTime.MinValue;
data.ValueTwo = DateTime.Now;
JsonSerializer serializer = new JsonSerializer();
using (StringWriter writer = new StringWriter())
{
serializer.Serialize(writer, data);
Console.Write(writer.ToString());
}
Console.ReadKey();
}
Console output:
{"ValueOne":null,"ValueTwo":"2016-10-26T09:54:48.497463+01:00"}
Custom DateTime Json Converter
public class DateTimeConverter : JsonConverter
{
public override void WriteJson(JsonWriter jsonWriter, object inputObject,JsonSerializer jsonSerializer)
{
// Typecast the input object
var dateTimeObject = inputObject as DateTime?;
// Set the properties of the Json Writer
jsonWriter.Formatting = Newtonsoft.Json.Formatting.Indented;
if(dateTimeObject == DateTime.MinValue)
jsonWriter.WriteValue((DateTime?)null);
else
jsonWriter.WriteValue(dateTimeObject);
}
public override object ReadJson(JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
DateTime? readValue = reader.ReadAsDateTime();
return (readValue == null) ? DateTime.MinValue : readValue;
}
public override bool CanConvert(Type objectType)
{
return typeof(DateTime?).IsAssignableFrom(objectType);
}
}
Usage:
public class DateTest
{
[JsonConverterAttribute(typeof(DateTimeConverter))]
public DateTime? MyDateTime { get; set;}
[JsonConverterAttribute(typeof(DateTimeConverter))]
public DateTime? MyDateTime1 { get; set; }
}
void Main()
{
DateTest dateTest = new DateTest
{
MyDateTime = DateTime.MinValue,
MyDateTime1 = DateTime.MaxValue
};
Console.WriteLine(JsonConvert.SerializeObject(dateTest));
}
Result:
{
"MyDateTime": null,
"MyDateTime1": "9999-12-31T23:59:59.9999999"
}
Not 100% clear on why the original poster wanted this and it's 5 years down the line but I too wanted to achieve this.
However, in my case the issue was that when I serialised my object it was inserting the DateTime fields because they hadn't been specified on the incoming object but were being returned as MinValue not null.
Thus when I deserialised at the other end these fields were showing as 'dirty' and being parsed.
In this case you can just amend your JsonSerializerSettings block to include 'ignore' in the default value handling:
string serialisedMessage = JsonConvert.SerializeObject(message, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
});
So I'm leaving this in case of future users who are in my position.