Search code examples
c#json.netjsonschema

Generate JSON Schema for only required properties


We are using Newtonsoft JSON (C#, .Net Framework 4.5) and I would like generate a JSON schema based on a few properties of a class which has category attribute set. How do I achieve this?

Example:

class School
{
    [Editable(false)]
    [DisplayName("Student")]
    [Category("Student")]
    public string StudentName { get; set; }

    [Editable(true)]
    [Range(0, 1000, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    [Category("Student")]
    public int Id { get; set; } = 10;

    [Editable(false)]
    [DisplayName("ParentName")]
    [Category("Student")]
    public string ParentName { get; set; }


    [JsonProperty("ParentPhone", Required = Required.Default)]
    [PhoneMask("999-999-9999",
        ErrorMessage = "{0} value does not match the mask {1}.")]
    [Category("Student")]
    public string Phone;

    [Editable(true)]
    [Category("Staff")]
    public string TeacherName { get; set; }

    [Editable(true)]
    [Range(0, 10000, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    [DefaultValue(5000)]
    [Category("Staff")]
    public int Salary { get; set; }

    [ReadOnly(true)]
    [Category("Student")]
    public bool AvailingTransport { get; set; } = true;
}

From the above example, I would like to generate JSON schema for only those properties which has category attribute set to student only.

I tried something like below, which didn't work.

abstract class SelectedAttributesSerializationProvider : JSchemaGenerationProvider
{
    public override JSchema GetSchema(JSchemaTypeGenerationContext context)
    {
        var type = context.ObjectType;
        JSchema schema = null;

        var generator = new JSchemaGenerator();
        PropertyInfo[] myMembers = type.GetProperties();
       
        var categoryAttributes = myMembers.Where(c => c.CustomAttributes.Any(p => p.AttributeType.Name == "CategoryAttribute"));

        foreach(PropertyInfo categoryAttribute in categoryAttributes)
        {
            foreach(CustomAttributeData customAttributeData in categoryAttribute.GetCustomAttributesData())
            {
                if(customAttributeData.AttributeType.Name == "CategoryAttribute")
                {
                    generator.GenerationProviders.Add(this);
                }
            }           
        }
       
        schema = generator.Generate(type);
        return ModifySchema(schema, context);
    }

    public abstract JSchema ModifySchema(JSchema schema, JSchemaTypeGenerationContext context);

}

Solution

  • Rather than a custom JSchemaGenerationProvider, it will be easier to accomplish what you need with a custom contract resolver that only returns the properties marked with the required category.

    First define the following contract resolver that removes all properties other than those with the required CategoryAttribute.Category metadata value:

    public class CategoryOnlyContractResolver : DefaultContractResolver
    {
        readonly bool alwaysIncludeConstructorParameters = false;
        readonly string category;
    
        public CategoryOnlyContractResolver(string category, bool alwaysIncludeConstructorParameters = false) : base() 
        {
            this.category = category;
            this.alwaysIncludeConstructorParameters = alwaysIncludeConstructorParameters; 
        }
    
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {
            var contract = base.CreateObjectContract(objectType);
            for (int i = contract.Properties.Count - 1; i >= 0; i--)
            {
                var p = contract.Properties[i];
                if (!p.AttributeProvider.GetAttributes(typeof(System.ComponentModel.CategoryAttribute), true).Cast<CategoryAttribute>().Any(c => c.Category == category)
                    || (alwaysIncludeConstructorParameters && contract.CreatorParameters.GetClosestMatchProperty(p.PropertyName) != null))
                    contract.Properties.RemoveAt(i);
            }
            return contract;
        }
    }
    

    Now you can generate your schema as follows:

    var studentGenerator = new JSchemaGenerator();
    studentGenerator.ContractResolver = new CategoryOnlyContractResolver("Student");
    
    var studentSchema = studentGenerator.Generate(typeof(School));
    

    Notes:

    • The contract resolver will optionally retain properties that are also constructor parameters. (This does not apply to the example shown in your question.)

    • One advantage of this approach is that you can later reuse the same contract resolver to serialize only the properties with the required category as shown in Serialization using ContractResolver.

    Demo fiddle here.