Ignore property with attribute in custom JSON converter - c#

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

Related

Custom JsonConvertor that also serializes it's value minus one of it's properties

I am having trouble with the requirement to serialize an object in a specific way whereby the object id value becomes the key and the rest of the object forms the value.
Simplified class to be serialized:
[JsonConverter(typeof(FieldTypeConvertor))]
public class FieldType {
public string Id { get; set; }
public string Condition { get; set; }
public string FieldType { get; set; }
public string Label { get; set; }
public string Options { get; set; }
}
Here is my JsonConvertor WriteJson method:
public override void WriteJson(JsonWriter writer, UmbracoFormFieldDto value, JsonSerializer serializer)
{
var props = value.GetType().GetProperties();
var idProp = props.FirstOrDefault(p => p.Name.Equals("id", StringComparison.OrdinalIgnoreCase));
var key = idProp.GetValue(value, null).ToString();
var newObj = JsonConvert.SerializeObject(value, new JsonSerializerSettings()
{ ContractResolver = new IgnorePropertiesResolver(new[] { "id" }) });
var container = new JObject { { key, newObj } };
container.WriteTo(writer);
}
I get why I end up with a StackOverflow but do not know how to avoid it in order generate the output I need which is the following:
"idValueFromOriginalObj": {
"condition": "propValue",
"fieldype": "propValue",
"label": "propValue",
"options": "propValue"
}
Essentially, the value of id in the original object becomes the key in the serialized object and the rest of the original object properties form the value.
Your problem is that, inside JsonConverter.ReadJson(), you are attempting to recursively serialize your value object, but since the converter is applied directly to the type using [JsonConverter(typeof(TConverter))], you are getting a stack overflow.
There are several options to disable a converter for recursive serialization; JSON.Net throws StackOverflowException when using [JsonConvert()] details some of them. However, since you are already using a custom contract resolver IgnorePropertiesResolver to ignore properties named "id", you might enhance the resolver to also ignore converters of type FieldTypeConvertor. The following should do the trick:
public class IgnorePropertiesResolver : DefaultContractResolver
{
readonly HashSet<string> propertiesToIgnore;
readonly HashSet<Type> converterTypesToIgnore;
public IgnorePropertiesResolver(IEnumerable<string> propertiesToIgnore, IEnumerable<Type> converterTypesToIgnore) : base() =>
(this.propertiesToIgnore, this.converterTypesToIgnore) =
((propertiesToIgnore ?? throw new ArgumentNullException()).ToHashSet(StringComparer.OrdinalIgnoreCase),
(converterTypesToIgnore ?? throw new ArgumentNullException()).ToHashSet());
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (propertiesToIgnore.Contains(member.Name))
property.Ignored = true;
if (property.Converter != null && converterTypesToIgnore.Contains(property.Converter.GetType()))
property.Converter = null;
return property;
}
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
if (contract.Converter != null && converterTypesToIgnore.Contains(contract.Converter.GetType()))
contract.Converter = null;
return contract;
}
};
Then modify FieldTypeConvertor as follows:
public sealed class FieldTypeConvertor : JsonConverter<UmbracoFormFieldDto>
{
static readonly IContractResolver innerResolver = new IgnorePropertiesResolver(new [] { "id" }, new [] { typeof(FieldTypeConvertor) })
{
NamingStrategy = new CamelCaseNamingStrategy(),
};
public override void WriteJson(JsonWriter writer, UmbracoFormFieldDto value, JsonSerializer serializer)
{
var props = value.GetType().GetProperties();
var idProp = props.FirstOrDefault(p => p.Name.Equals("id", StringComparison.OrdinalIgnoreCase));
var key = idProp.GetValue(value, null).ToString();
writer.WriteStartObject();
writer.WritePropertyName(key);
JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = innerResolver }).Serialize(writer, value);
writer.WriteEndObject();
}
public override UmbracoFormFieldDto ReadJson(JsonReader reader, Type objectType, UmbracoFormFieldDto existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
}
And your model will be serialized as required:
{
"idValueFromOriginalObj": {
"condition": "propValue",
"fieldType": "propValue",
"label": "propValue",
"options": "propValue"
}
}
Notes:
Newtonsoft recommends you cache the contract resolver for best performance.
You should inherit from DefaultContractResolver instead of CamelCasePropertyNamesContractResolver for reasons explained in Json.Net: Html Helper Method not regenerating.
For performance reasons, I eliminated the intermediate serialization to JObject and instead serialized directly to the incoming JsonWriter.
Demo fiddle here.

Using Newtonsoft Json how cast an int value to string

I'm just starting to check how to serialize and deserialize using json for a project where I need to use ArangoDB.
At the moment, I have a test class AnoherTestPerson:
public class AnotherTestPerson
{
public AnotherTestPerson(int id, string fullname, int age)
{
this.Id = id;
this.Fullname = fullname;
this.Age = age;
}
public int Id { get; set; }
public string Fullname { get; set; }
public int Age { get; set; }
}
Now, I need to cast the Id value to a string, because ArangoDB doesn't work when you pass a numerical value as the _key, so I'm guessing I have to do that from the serializer that the Arango driver uses, because in the project I'm going to work on, we won't have access to the classes of the entities we want to store on the data base.
Any help would be appreciated, as I'm still learning how serialization works with Json and C#.
Here's the rest of the code:
public static async Task Main(string[] args)
{
string connectionString = "private";
var arango = new ArangoContext(cs:connectionString, settings:
new ArangoConfiguration
{
Serializer = new ArangoNewtonsoftSerializer(CustomDataContractResolver.Instance)
//Using custom contract resolver for automatically changing the Id name
//from the object class to _key in the Json file
}
);
await arango.Document.CreateAsync("TestDB", typeof(AnotherTestPerson).Name, testPerson);
}
Here's the custom contract resolver. I tried changing the type of the property here but it didn't work.
public class CustomDataContractResolver : DefaultContractResolver
{
public static readonly CustomDataContractResolver Instance = new CustomDataContractResolver();
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (property.PropertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
{
property.PropertyName = "_key";
if(property.PropertyType == Type.GetType("System.Int32"))
{
property.PropertyType = Type.GetType("System.String");
}
}
return property;
}
}
EDIT
So checking the comment by SBFrancies, I implemented a basic JsonConverter:
public class ToStringJsonConverted : Newtonsoft.Json.JsonConverter
{
public static readonly ToStringJsonConverted Instance = new ToStringJsonConverted();
public override bool CanConvert(Type objectType)
{
return true;
}
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)
{
writer.WriteValue(value.ToString());
}
}
and linked it to the custom ContractResolver:
public class CustomDataContractResolver : DefaultContractResolver
{
public static readonly CustomDataContractResolver Instance = new CustomDataContractResolver();
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (property.PropertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
{
property.PropertyName = "_key";
if(property.PropertyType == Type.GetType("System.Int32"))
{
property.Converter = ToStringJsonConverted.Instance;
}
}
return property;
}
}
It get's serialized as I wanted to, but the deserializing it's not working right now. I'll check how to read Json files and parse them for now.
I got the serializing and deserializing working, with help from #SBFrancies reference in the comments.
ContractResolver:
public class CustomDataContractResolver : DefaultContractResolver
{
public static readonly CustomDataContractResolver Instance = new CustomDataContractResolver();
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (property.PropertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
{
property.PropertyName = "_key";
if(property.PropertyType == Type.GetType("System.Int32"))
{
property.Converter = ToStringJsonConverted.Instance;
}
}
return property;
}
}
JsonConverter:
public class ToStringJsonConverted : Newtonsoft.Json.JsonConverter
{
public static readonly ToStringJsonConverted Instance = new ToStringJsonConverted();
public override bool CanConvert(Type objectType)
{
return true;
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
return Int32.Parse((string)reader.Value);
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
}

Using property attributes to define which JsonConverter to use

I have a need to specify a JsonConverter for properties which are decorated with a specific attribute, in this case [DataType(DataType.PostalCode)].
I already have a custom JsonConverter for which I have set the CanConvert method as follows:
public override bool CanConvert(Type objectType) => objectType == typeof(string);
How can I make sure the PostcodeJsonConverter is used instead when the API encounters a PostalCode property?
[DataType(DataType.PostalCode)]
public string Postcode { get; set; }
I've tried the following but I suspect the DataType attribute is not available at this point.
public override bool CanConvert(Type objectType) =>
objectType == typeof(string) &&
objectType.GetCustomAttributes(true)
.OfType<DataTypeAttribute>()
.Any(dta => dta.DataType == DataType.PostalCode);
Do I need to decorate my model as follows instead?
[DataType(DataType.PostalCode)]
[JsonConverter(typeof(PostcodeJsonConverter))]
public string Postcode { get; set; }
You can make a custom ContractResolver that looks for your DataType attribute on each property and maps the values to the appropriate converter. Here is the code you would need:
public class DataTypeResolver : DefaultContractResolver
{
private Dictionary<DataType, JsonConverter> ConvertersByDataType { get; set; }
public DataTypeResolver()
{
// Adjust this list to match your actual data types and converters
ConvertersByDataType = new Dictionary<DataType, JsonConverter>
{
{ DataType.PostalCode, new PostalCodeConverter() },
{ DataType.PhoneNumber, new PhoneNumberConverter() },
};
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty prop = base.CreateProperty(member, memberSerialization);
var att = prop.AttributeProvider.GetAttributes(true).OfType<DataTypeAttribute>().FirstOrDefault();
if (att != null)
{
JsonConverter converter;
if (ConvertersByDataType.TryGetValue(att.DataType, out converter))
{
prop.Converter = converter;
}
}
return prop;
}
}
Then pass the resolver to SerializeObject and/or DeserializeObject via the settings:
var settings = new JsonSerializerSettings
{
ContractResolver = new DataTypeResolver()
};
string json = JsonConvert.SerializeObject(yourObject, settings);
Here is a working demo: https://dotnetfiddle.net/k1kWv5
You can add Converters to the JsonSerializerSettings. So instead of decorating everything, you may as well add your PostcodeJsonConverter there (depending on how often it is used, a decorator may be better though):
For aspnet core defaults:
services.AddMvc().AddJsonOptions(o => o.SerializerSettings.Converters.Add(new PostcodeJsonConverter()))
For JsonConvert:
JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
Converters = { new PostcodeJsonConverter() }
});

JSON.NET Serialize DateTime.MinValue as null

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.

How can I control the converter used for dictionary entries?

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 ());

Categories

Resources