Looking for help with Newtonsoft Json on asp.net core 2.2.
I have a JsonEnumConverter<T> which was responsible for serializing/deserializing values from DescriptionAttribute from an Enum type. It was working fine until about 2 weeks ago and now it has completely stopped working.
here's what I have:
//From PerformersController:
public async Task<ActionResult<PagedPerformers>> GetPagedPerformersAsync([FromQuery] PerformerRequest performerRequest) { ... }
[JsonObject]
public class PerformerRequest : PageRequest
{
[FromQuery(Name = "performer_id")]
[JsonProperty(PropertyName = "performer_id", Order = 1)]
public override string Id { get; set; }
....
}
[JsonConverter(typeof(JsonEnumConverter<SortDirectionType>))]
public enum SortDirectionType
{
[Description("asc")]
ASCENDING,
[Description("desc")]
DESCENDING
}
public abstract class PageRequest
{
[FromQuery(Name = "page")]
[JsonProperty("page")]
public int Page { get; set; }
[FromQuery(Name = "limit")]
[JsonProperty("limit")]
public int PageSize { get; set; } = 100;
[FromQuery(Name = "sort_field")]
[JsonProperty("sort_field")]
public string SortField { get; set; } //= "Id";
[FromQuery(Name = "sort_dir")] [JsonConverter(typeof(JsonEnumConverter<SortDirectionType>))]
[JsonProperty("sort_dir")]
public SortDirectionType SortDirection { get; set; }
[FromQuery(Name = "id")]
[JsonProperty("id")]
public virtual string Id { get; set; }
}
public class JsonEnumConverter<T> : JsonConverter where T : struct, IComparable, IConvertible, IFormattable
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var type = typeof(T);
if (!type.IsEnum) throw new InvalidOperationException();
var enumDescription = (string)reader.Value;
return enumDescription.GetEnumValueFromDescription<T>();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var type = typeof(T);
if (!type.IsEnum) throw new InvalidOperationException();
if (value != null)
{
if (value is Enum sourceEnum)
{
writer.WriteValue(sourceEnum.GetDescriptionFromEnumValue());
}
}
}
}
public static class EnumExtensions
{
public static string GetDescriptionFromEnumValue(this Enum #enum)
{
FieldInfo fi = #enum.GetType().GetField(#enum.ToString());
DescriptionAttribute[] attributes =
(DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute),
false);
if (attributes != null &&
attributes.Length > 0)
return attributes[0].Description;
else
return #enum.ToString();
}
public static T GetEnumValueFromDescription<T>(this string description)
{
var type = typeof(T);
if (!type.IsEnum)
throw new InvalidOperationException();
foreach (var field in type.GetFields())
{
if (Attribute.GetCustomAttribute(field,
typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
{
if (attribute.Description == description)
return (T)field.GetValue(null);
}
else
{
if (field.Name == description)
return (T)field.GetValue(null);
}
}
throw new ArgumentException($"No matching value for enum {nameof(T)} found from {description}.",$"{nameof(description)}"); // or return default(T);
}
}
this was working absolutely fine until recently. Now I'm not sure whats going on I get ValidationProblemDetails response right away. If I suppress asp.net core 2.2 model state invalid filter then modelState.IsValid will still have false. If I put a breakpoint in ReadJson of my JsonEnumConverter it wont even hit. Even tried to set JsonSerializerSettings in startup with no success or luck. Have already tried replacing Description with EnumMember and StringEnumConverter. Still the same issue. Seems like there is some issue with ModelBinder and Json.NET not playing well with each other.
NOTE: This issue is happening on ASP.NET Core 2.2. Suggesting solutions for 3.0 is not helpful!!
If you are using aspnet core 3 / netstandard 2.1, you can try this library https://github.com/StefH/System.Text.Json.EnumExtensions which defines some extensions to the JsonStringEnumConverter to support attributes like EnumMember, Display and Description.
Related
I want set property names at runtime. I already achieve this for serialization.
For example. I have a simple model like as below:
[JsonConverter(typeof(DataModelConverter))]
public class DataModel
{
public string Name { get; set; }
public int Age { get; set; }
}
And I have a simple DataModelConverter, that inherited from JsonConverter:
public class DataModelConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type type = value.GetType();
JObject jo = new JObject();
foreach (PropertyInfo prop in type.GetProperties())
{
jo.Add(prop.Name == "Name" ? "FullName" : prop.Name, new JValue(prop.GetValue(value)));
}
jo.WriteTo(writer);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DataModel);
}
}
And I have a simple controller like as below:
[Route("api/[controller]")]
[ApiController]
public class NewtonController : ControllerBase
{
public IEnumerable<DataModel> GetNewtonDatas([FromBody] DataModel input)
{
return new List<DataModel>()
{
new DataModel
{
Name="Ramil",
Age=25
},
new DataModel
{
Name="Yusif",
Age=26
}
};
}
}
If I call this API, result will like as below (Showing FullName Instead of Name):
[
{
"FullName": "Ramil",
"Age": 25
},
{
"FullName": "Yusif",
"Age": 26
}
]
But I have a problem. This is not working for deserialization.
For example: If I call this API with this body, then Name will null.
{
"FullName":"Ramil"
}
My attribute is not working for deserialization. I want set property name via attribute for deserialization at runtime .
I don't want use some middleware, I want to achieve this only by using the any attribute at runtime. I must read JSON property names from my appsettings.json file.
Thanks for help!
You have overridden CanRead to return false:
public override bool CanRead
{
get { return false; }
}
This causes Json.NET not to call your your converter's DataModelConverter.ReadJson() method during deserialization, and instead use default deserialization. Since "FullName" does not have the same (case-invariant) name as the Name property, it never gets set, and remains null.
To fix this, remove the override for CanRead (the default implementation returns true) and implement ReadJson(), e.g. as follows:
public class DataModelConverter : NameRemappingConverterBase
{
static string AlternateName => "FullName";
static string OriginalName => "Name";
public override bool CanConvert(Type objectType) => objectType == typeof(DataModel);
// Replace the below logic with name mappings from appsettings.json
protected override string ToJsonPropertyName(JsonProperty property) =>
string.Equals(property.UnderlyingName, OriginalName, StringComparison.OrdinalIgnoreCase) ? AlternateName : base.ToJsonPropertyName(property);
protected override string FromJsonPropertyName(string name) =>
string.Equals(name, AlternateName, StringComparison.OrdinalIgnoreCase) ? OriginalName : base.FromJsonPropertyName(name);
}
public abstract class NameRemappingConverterBase : JsonConverter
{
protected virtual string ToJsonPropertyName(JsonProperty property) => property.PropertyName;
protected virtual string FromJsonPropertyName(string name) => name;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
var value = existingValue ?? contract.DefaultCreator();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
if (reader.TokenType != JsonToken.PropertyName)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
var name = FromJsonPropertyName((string)reader.Value);
reader.ReadToContentAndAssert();
var property = contract.Properties.GetProperty(name, StringComparison.OrdinalIgnoreCase);
if (!ShouldDeserialize(property))
{
reader.Skip();
}
else
{
var propertyValue = serializer.Deserialize(reader, property.PropertyType);
property.ValueProvider.SetValue(value, propertyValue);
}
}
return value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
writer.WriteStartObject();
foreach (var property in contract.Properties.Where(p => ShouldSerialize(p, value)))
{
var propertyValue = property.ValueProvider.GetValue(value);
if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore)
continue;
var name = ToJsonPropertyName(property);
writer.WritePropertyName(name);
serializer.Serialize(writer, propertyValue);
}
writer.WriteEndObject();
}
protected virtual bool ShouldDeserialize(JsonProperty property) =>
property != null && property.Writable;
protected virtual bool ShouldSerialize(JsonProperty property, object value) =>
property.Readable && !property.Ignored && (property.ShouldSerialize == null || property.ShouldSerialize(value));
}
public static partial class JsonExtensions
{
public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
Demo fiddle here.
here is my code:
Classes:
[Serializable]
public abstract class ChallengeDetailsDto : ChallengeDto
{
[JsonConverter(typeof(PagesConverter))]
public PageDto[] Pages { get; set; } = new PageDto[] { };
}
[Serializable]
public class CaseDto : ChallengeDto
{
public CaseDto()
{
Discriminator = CaseDtoDiscriminator;
}
}
Controller:
[HttpPost]
public async Task<IActionResult> CreateAsync([FromForm]CaseDetailsDto dto)
{
}
Custom Converter
internal class ChallengeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
if (objectType == typeof(ChallengeDto))
{
return true;
}
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
ChallengeDto challengeDto = null;
var jsonChallange = JObject.Load(reader);
var discriminator = jsonChallange.Properties().First(p => p.Name == "discriminator").Value.ToString();
if (discriminator == ChallengeDto.CaseDtoDiscriminator)
{
challengeDto = (CaseDto)JsonConvert.DeserializeObject(jsonChallange.ToString(), typeof(CaseDto));
}
/*...*/
return challengeDto;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var challenge = value as ChallengeDto;
if (challenge.Discriminator == ChallengeDto.CaseDtoDiscriminator)
{
serializer.Serialize(writer, (CaseDto)challenge);
}
/*...*/
}
}
Error:
Could not create an instance of type
'Controllers.Challenges.Dtos.PageDto'. Model bound complex types must
not be abstract or value types and must have a parameterless
constructor.
If I use [FromBody], every thing works fine. From what I can understand, using [FromForm] the binding does not use Json.
What is the best way to bind Form Values to the property in this case?
Let's say I have the following custom JsonConverter for serialization and/or deserialization:
public class VersionConverter : JsonConverter<Version>
{
public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer)
{
string s = (string)reader.Value;
return new Version(s);
}
}
public class NuGetPackage
{
public string PackageId { get; set; }
public Version Version { get; set; }
public string Description { get; set; }
}
Let's say I have the following code snippet in my application:
NuGetPackage p1 = new NuGetPackage
{
PackageId = "Newtonsoft.Json",
Version = new Version(10, 0, 4),
Description = null
};
string json = JsonConvert.SerializeObject(p1, Formatting.Indented, new VersionConverter());
I want the Json.NET converter to Ignore the Description member variable of the NuGetPackage class.
Note: I do Not Want to use the following "marker boolean" member variable:
public bool ShouldSerializeINSERT_YOUR_PROPERTY_NAME_HERE()
{
if(someCondition){
return true;
}else{
return false;
}
}
I would rather specify the ignoring of a specific member variable somewhere
a) when my code invokes the JsonConvert.SerializeObject?
b) or within the VersionConverter code class itself?
Could someone please show me how to ignore the specific member variable in such a way?
Since NuGetPackage is fairly simple, you could just write an additional JsonConverter for NuGetPackage that serializes only the members you need, e.g.:
public class SimplifiedNuGetPackageConverter : JsonConverter
{
public override bool CanConvert(Type objectType) { return objectType == typeof(NuGetPackage); }
public override bool CanRead { get { return false; } }
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)
{
var package = (NuGetPackage)value;
serializer.Serialize(writer, new { package.PackageId, package.Version });
}
}
Then serialize as follows:
var settings = new JsonSerializerSettings
{
Converters = { new VersionConverter() },
};
if (!someCondition)
settings.Converters.Add(new SimplifiedNuGetPackageConverter());
var json = JsonConvert.SerializeObject(p1, Formatting.Indented, settings);
If you are serializing multiple instances of NuGetPackage at once and need to write Description for some but not all, you could add the logic for someCondition inside WriteJson() itself:
public class ConditionalNuGetPackageConverter : JsonConverter
{
public override bool CanConvert(Type objectType) { return objectType == typeof(NuGetPackage); }
public override bool CanRead { get { return false; } }
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)
{
var package = (NuGetPackage)value;
// Replace with your logic:
var someCondition = !string.IsNullOrWhiteSpace(package.Description);
if (someCondition)
serializer.Serialize(writer, new { package.PackageId, package.Version, package.Description });
else
serializer.Serialize(writer, new { package.PackageId, package.Version });
}
}
And then serialize as follows:
var settings = new JsonSerializerSettings
{
Converters = { new ConditionalNuGetPackageConverter(), new VersionConverter() },
};
var json = JsonConvert.SerializeObject(p1, Formatting.Indented, settings);
Working .Net fiddle here.
I am using Swashbuckle.AspNetCore.Swagger (1.0.0) and Swashbuckle.AspNetCore.SwaggerGen (1.0.0). I am trying to add default examples to my API following Default model example in Swashbuckle (Swagger). I created a new class file and added,
public class SwaggerDefaultValue : Attribute
{
public string ParameterName { get; set; }
public string Value { get; set; }
public SwaggerDefaultValue(string parameterName, string value)
{
this.ParameterName = parameterName;
this.Value = value;
}
}
public class AddDefaultValues : IOperationFilter
{
public void Apply(Operation operation, DataTypeRegistry dataTypeRegistry, ApiDescription apiDescription)
{
foreach (var param in operation.Parameters)
{
var actionParam = apiDescription.ActionDescriptor.GetParameters().First(p => p.ParameterName == param.Name);
if (actionParam != null)
{
var customAttribute = actionParam.ActionDescriptor.GetCustomAttributes<SwaggerDefaultValue>().FirstOrDefault();
if (customAttribute != null)
{
param.DefaultValue = customAttribute.Value;
}
}
}
}
}
but I get this error - AddDefaultValues does not implement interface member IOperationFilter.Apply(Operation, OperationFilterContext)
That link you are following is not for the Swashbuckle.AspNetCore version
Look in the correct project for the proper examples:
https://github.com/domaindrivendev/Swashbuckle.AspNetCore/search?q=IOperationFilter&unscoped_q=IOperationFilter
In a ASP.NET Web API project, I've got a custom type with a TypeConverter that handles the to and from for string conversions, as described here:
http://blogs.msdn.com/b/jmstall/archive/2012/04/20/how-to-bind-to-custom-objects-in-action-signatures-in-mvc-webapi.aspx
The custom type is used as a property in my model and this works great. The json for my model object includes my custom object as the string value created from my TypeConverter.
So then I switch the request to accept XML instead and the TypeConverter is no longer invoked and my object is incorrectly serialized.
How to I get the XML serializer to utilize the TypeConverter as the JSON one does. The article referenced made it sound like the use of TypeConverters, when present, was a predictable feature. If it is completely at the whim of the serializer then making APIs that express data in both XML and JSON in a consistent way is nearly impossible.
Sample code from the article, plus a bit of context:
[TypeConverter(typeof(LocationTypeConverter))]
public class Location
{
public int X { get; set; }
public int Y { get; set; }
// Parse a string into a Location object. "1,2" --> Loc(X=1,Y=2)
public static Location TryParse(string input)
{
var parts = input.Split(',');
if (parts.Length != 2)
{
return null;
}
int x,y;
if (int.TryParse(parts[0], out x) && int.TryParse(parts[1], out y))
{
return new Location { X = x, Y = y };
}
return null;
}
public override string ToString()
{
return string.Format("{0},{1}", X, Y);
}
}
public class LocationTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
return Location.TryParse((string) value);
}
return base.ConvertFrom(context, culture, value);
}
}
Create a model with that class:
public class SampleModel
{
public int One { get; set;}
public string Two { get; set;}
public Location Three { get; set;}
}
Create a ApiController with a method like this:
public SampleModel Get()
{
return new SapleModel { One = 1, Two = "Two", Three = new Location { X = 111, Y = 222 } };
}
That's it. Using fiddler and try them both and note that Location serializes using the converter into the single-line comma-seperated format like this (Three="111,222") with Json but not with XML.