Ivan
Ivan

Reputation: 49

Serialize C# class or .net core model with data annotation attributes to JSON format

I know that there is a way but what is the right way to convert(serialize) .NET Core Model Class (C# class properties) from this:

[Required]
[DataType(DataType.Text)]

public string Name {get;set;}
[Required, DataType(DataType.EmailAddress)]

public string Email {get;set;}
[Required]
[DataType(DataType.Text)]


public string Subject{get;set;}
[Required]
[MaxLength(500)]
[Required, DataType(DataType.MultilineText)]

public string Message{get;set;}
[Required]
[MaxLength(500)]
[Required, DataType(DataType.DateTime)]

public DateTime DateOfBirth{get;set;}

public int AnyNumber{get;set;}

To this:


[
    {
        "name": 
        {
            "value": "",
            "type": "text",
            "validations": 
            {
                "required": true
            } 
        }
    },
    {
        "email": 
        {
            "value": "",
            "type": "email",
            "validations": 
            {
                "required": true
            } 
        },
        "message": 
        {
            "value": "",
            "type": "multilineText",
            "validations": 
            {
                "required": true,
                "maxLength": 500
            } 
        },
        "anyNumber": 
        {
            "value": 0,
            "type": "number",
            "validations": 
            {
                "required": false
            } 
        }

    },
]

So any class that I provide structured like in the provided snipped image to get JSON serialized response.
I have found and tried some solutions but things for me get very complicated and I know that reflection is expensive so I have to use it smartly.
Thank you in advance.

Upvotes: 0

Views: 2133

Answers (1)

Lev
Lev

Reputation: 593

Here is a rough implementation of what you need, using the .NET Core System.Text.Json

How to write custom converters for JSON serialization (marshalling) in .NET

public static class SerializationTester
{
    public static void TestSerialize()
    {
        TestClass test = new TestClass()
        {
            AnyNumber = 3,
            DateOfBirth = DateTime.UtcNow,
            Email = "[email protected]",
            Message = "hello world!",
            Name = "john smith",
            Subject = "some subject"
        };
        var options = new JsonSerializerOptions()
        {
            WriteIndented = true,
            IgnoreNullValues = true
        };
        options.Converters.Add(new MetadataConverter());
        var serialized = JsonSerializer.Serialize(test, options);
        Console.WriteLine(serialized);
    }
}

public class MetadataConverter : JsonConverterFactory
{
    /// <summary>
    /// contain nul metadata for types that don't have metdata attributes and dont' need custom converters
    /// each type will only be parsed once with reflections, obviously the attribute values are identical for all class instances
    /// </summary>
    private Dictionary<Type, Dictionary<PropertyInfo, Metadata>> typesMetadataCache = new Dictionary<Type, Dictionary<PropertyInfo, Metadata>>();
    public override bool CanConvert(Type typeToConvert)
    {
        Dictionary<PropertyInfo, Metadata> typeMeta;
        if (!typesMetadataCache.TryGetValue(typeToConvert, out typeMeta))
        {
            typesMetadataCache[typeToConvert] = typeMeta = GetTypeMeta(typeToConvert);
        }
        return typeMeta != null;
    }

    private Dictionary<PropertyInfo, Metadata> GetTypeMeta(Type typeToConvert)
    {
        Dictionary<PropertyInfo, Metadata> theReturn = new Dictionary<PropertyInfo, Metadata>();
        bool metadataSpecified = false;
        foreach (var currentProperty in typeToConvert.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            var required = currentProperty.GetCustomAttributes<RequiredAttribute>()?.FirstOrDefault();
            var dataType = currentProperty.GetCustomAttributes<DataTypeAttribute>()?.FirstOrDefault();
            var maxLength = currentProperty.GetCustomAttributes<MaxLengthAttribute>()?.FirstOrDefault();
            if (required != null || dataType != null || maxLength != null)
            {
                metadataSpecified = true;
            }

            var currentMeta = theReturn[currentProperty] = new Metadata()
            {
                type = dataType?.DataType.ToString() ?? currentProperty.PropertyType.Name,
                validations = new Validations()
                {
                    maxLength = maxLength?.MaxLength,
                    required = (required != null)
                }
            };
        }
        if (metadataSpecified)
        {
            return theReturn;
        }
        // metadata not specified for any property, don't use custom converter
        return null;
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        return (JsonConverter)Activator.CreateInstance(typeof(MetadataTypeConverter<>).MakeGenericType(typeToConvert), options, typesMetadataCache[typeToConvert]);
    }

    private class MetadataTypeConverter<TValue> : JsonConverter<TValue>
    {
        private Dictionary<PropertyInfo, JsonConverter> propertyConverters = new Dictionary<PropertyInfo, JsonConverter>();
        public MetadataTypeConverter(JsonSerializerOptions options, Dictionary<PropertyInfo, Metadata> typeMetadata)
        {
            foreach (var currentMeta in typeMetadata)
            {
                if (currentMeta.Value == null)
                {
                    propertyConverters[currentMeta.Key] = options.GetConverter(currentMeta.Key.PropertyType);
                }
                else
                {
                    propertyConverters[currentMeta.Key] = (JsonConverter)Activator.CreateInstance(typeof(MetadataValueConverter<>).MakeGenericType(currentMeta.Key.PropertyType), options, currentMeta.Value);
                }
            }
        }
        public override TValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            throw new NotImplementedException();
        }

        public override void Write(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options)
        {
            writer.WriteStartObject();
            foreach (var currentConverter in propertyConverters)
            {
                var currentPropertyValue = currentConverter.Key.GetValue(value);
                if (currentConverter.Value is IMetadataValueConverter currentMetadataValueConverter)
                {
                    currentMetadataValueConverter.Write(writer, currentPropertyValue, options);
                }
                else
                {
                    var currentWriteMethod = currentConverter.Value.GetType().GetMethod("Write", BindingFlags.Public | BindingFlags.Instance);
                    currentWriteMethod.Invoke(currentConverter.Value, new object[] { writer, currentPropertyValue, options });
                }
            }
            writer.WriteEndObject();
        }
    }
    private class MetadataValueConverter<TValue> : JsonConverter<TValue>, IMetadataValueConverter
    {
        private Metadata metadata;
        public MetadataValueConverter(JsonSerializerOptions options, Metadata metadata)
        {
            this.metadata = metadata;
        }
        public override TValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = JsonSerializer.Deserialize<MetadataWithValue>(ref reader, options);
            return value.value;
        }

        public override void Write(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, new MetadataWithValue(metadata, value), options);
        }
        void IMetadataValueConverter.Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => Write(writer, (TValue)value, options);
        public class MetadataWithValue : Metadata
        {
            public MetadataWithValue() { }
            public MetadataWithValue(Metadata metadata, TValue value)
            {
                type = metadata.type;
                validations = metadata.validations;
                this.value = value;
            }
            public TValue value { get; set; }
        }
    }

    private interface IMetadataValueConverter
    {
        void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options);
    }
    private class Metadata
    {
        public string type { get; set; }
        public Validations validations { get; set; }
    }
    private class Validations
    {
        public bool required { get; set; }
        public int? maxLength { get; set; }

    }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class RequiredAttribute : Attribute { }
public enum DataType
{
    Text,
    EmailAddress,
    MultilineText,
    DateTime
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DataTypeAttribute : Attribute
{
    public DataTypeAttribute(DataType dataType)
    {
        DataType = dataType;
    }

    public DataType DataType { get; private set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class MaxLengthAttribute : Attribute
{
    public MaxLengthAttribute(int maxLength)
    {
        MaxLength = maxLength;
    }

    public int MaxLength { get; private set; }
}
public class TestClass
{
    [Required]
    [DataType(DataType.Text)]
    public string Name { get; set; }

    [Required, DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Text)]
    public string Subject { get; set; }

    [Required]
    [MaxLength(500)]
    [Required, DataType(DataType.MultilineText)]
    public string Message { get; set; }

    [Required]
    [MaxLength(500)]
    [Required, DataType(DataType.DateTime)]
    public DateTime DateOfBirth { get; set; }

    public int AnyNumber { get; set; }
}

Upvotes: 1

Related Questions