Search code examples
asp.net-coreserializationjson.netdeserialization

JsonConverterAttribute is not working for Deserialization in ASP.NET Core 3.1 / 5.0


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!


Solution

  • 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.