Reputation: 8745
I want to use a custom JsonConverter for string arrays (or IEnumerable) and do some manipulations on the array (actually removing all strings that are null or whitespace).
But I am already stuck in the ReadJson method not knowing how to correctly get the string[].
I did a custom converter for simple strings where I checked for JsonToken.String. But arrays have StartArray and EndArray...
Anyone who already de/serialized their custom string arrays and could help me out?
More details:
What I want to achieve is centralized or optional string trimming on model binding (so I don't have to do that in every controller) and model validation checking for duplicates would detect "a string" and " a string" as duplicate.
I am trying to do that as JsonConverters (digging down model bindign log output, .net core docs, .net core github code brought me to the point a json converter is best).
Centralized usage would be configured in the StartUp Json Options:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions
(
options =>
{
options.SerializerSettings.Converters.Add(new TrimmedStringConverter());
options.SerializerSettings.Converters.Add(new CleanStringArrayConverter());
}
);
}
Usage on a per model basis it would look like
public class RequestModel
{
[JsonConverter(typeof(TrimmedStringConverter))]
public string MyValue { get; set; }
[JsonConverter(typeof(CleanStringArrayConverter))]
public string[] Entries { get; set; }
}
This question provided the converter for automatically trim strings on model binding. I just added some salt.
public class TrimmedStringConverter : JsonConverter
{
public bool EmptyStringsAsNull { get; }
public TrimmedStringConverter()
{
EmptyStringsAsNull = true;
}
public TrimmedStringConverter(bool emptyStringsAsNull)
{
EmptyStringsAsNull = emptyStringsAsNull;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
private string CleanString(string str)
{
if (str == null) return null;
str = str.Trim();
if (str.Length == 0 && EmptyStringsAsNull) return null;
return str;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
{
//if (reader.Value != null)
return CleanString(reader.Value as string);
}
return reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var text = (string)value;
if (text == null)
writer.WriteNull();
else
writer.WriteValue(CleanString(text));
}
}
Now this leaves my model having empty strings or nulls in the string[]. Which I now try to to automatically remove in a second converter (or a converter doing the same above but for string arrays, collections).
I just can't figure out how to correctly handle the array serialization/deserialization with the reader and serializer.
That's how far I got (a thank you to Silvermind). A second converter for string arrays.
First I managed to use the globally registered TrimmedStringConverter in the CleanStringArrayConverter (check the additional out-commented code) too. This worked as long as the TrimmedStringConverter was used globally and the CleanStringArrayConverter was on a per model basis. Using both globally cause infinite loops and SERVER CRASHES with an Access Violation exception.
So I changed it to this version where both can be registered globally side by side.
Unfortunatly it will only work for arrays.
May once someone of you will find this code, uses it and can share improvements?
public class CleanStringArrayConverter : JsonConverter
{
public bool TrimStrings { get; }
public bool EmptyStringsAsNull { get; }
public bool RemoveWhitespace { get; }
public bool RemoveNulls { get; }
public bool RemoveEmpty { get; }
public CleanStringArrayConverter()
{
TrimStrings = true;
EmptyStringsAsNull = true;
RemoveWhitespace = true;
RemoveNulls = true;
RemoveEmpty = true;
}
public CleanStringArrayConverter(bool trimStrings = true, bool emptyStringsAsNull = true, bool removeWhitespace = true, bool removeEmpty = true, bool removeNulls = true)
{
TrimStrings = trimStrings;
EmptyStringsAsNull = emptyStringsAsNull;
RemoveWhitespace = removeWhitespace;
RemoveNulls = removeNulls;
RemoveEmpty = removeEmpty;
}
private string CleanString(string str)
{
if (str == null) return null;
if (TrimStrings) str = str.Trim();
if (str.Length == 0 && EmptyStringsAsNull) return null;
return str;
}
private string[] CleanStringCollection(IEnumerable<string> strings)
{
if (strings == null) return null;
return strings
.Select(s => CleanString(s))
.Where
(
s =>
{
if (s == null) return !RemoveNulls;
else if (s.Equals(string.Empty)) return !RemoveEmpty;
else if (string.IsNullOrWhiteSpace(s)) return !RemoveWhitespace;
else return true;
}
)
.ToArray();
}
public override bool CanConvert(Type objectType)
{
return objectType.IsArray && objectType.GetElementType() == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string[] arr = null; // use null as default value
//string[] arr = new string[]; // use empty array as default value
// deserialze the array
if (reader.TokenType != JsonToken.Null)
{
if (reader.TokenType == JsonToken.StartArray)
{
// this one respects other registered converters (e.g. the TrimmedStringConverter)
// but causes server crashes when used globally due to endless loops
//arr = serializer.Deserialize<string[]>(reader);
// this doesn't respect others!!!
JToken token = JToken.Load(reader);
arr = token.ToObject<string[]>();
}
}
// clean up the array
if (arr != null) arr = CleanStringCollection(arr);
return arr;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
string[] arr = (string[])value;
if (value == null)
{
writer.WriteNull();
return;
}
arr = CleanStringCollection(arr);
// endless loops and server crashes!!!
//serializer.Serialize(writer, arr);
writer.WriteStartArray();
string v;
foreach(string s in arr)
{
v = CleanString(s);
if (v == null)
writer.WriteNull();
else
writer.WriteValue(v);
}
writer.WriteEndArray();
}
}
Upvotes: 4
Views: 10624
Reputation: 5944
It is basically the same idea:
internal sealed class TrimmedStringCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsArray && objectType.GetElementType() == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (existingValue is null)
{
// Returning empty array???
return new string[0];
}
var array = (string[])existingValue;
return array.Where(s => !String.IsNullOrEmpty(s)).ToArray();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value);
}
}
Perhaps you might want to do the same for the write part.
Upvotes: 2