Mixon
Mixon

Reputation: 33

Adding options property while generating json schema using JSchemaGenerator

I am using Newtonsoft's Json.NET Schema schema generator and I want to generate a JSON schema and hide few fields. I know that it is possible with the use of options property. Here is a sample schema that uses this property.

{
  "title": "Person",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "options": { "hidden": true },
      "description": "First and Last name",
      "minLength": 4,
      "default": "Jeremy Dorn"
    }
  }
} 

I have a class that is a base for the schema and I have decided to use a custom attribute on properties that I want to hide during schema generation. Then using custom GenerationProvider I want to check if the field has the attribute and if it does then add the "options": { "hidden": true }, bit.

The problem is that JSchema class doesn't have the Hidden property (like the JsonSchema class had previously) nor the Options property.

Note: I don't want to use the [JsonIgnore] as I need those properties serialized in some places, but I only want them hidden while creating the schema.

Any ideas how to achieve this?

Upvotes: 0

Views: 2246

Answers (3)

Mixon
Mixon

Reputation: 33

This is a rather long reply to @dbc which helped me to get this done. As my classes that I create the schema for are quite big and contain loads of different types inside them I couldn't get this solution to work. Few things that I noticed here. I am using "Newtonsoft.Json.Schema" Version="3.0.14" and inside the custom provider the line

var contract = (JsonObjectContract)context.Generator.ContractResolver.ResolveContract(context.ObjectType);

was throwing exception as the casting to JsonObjectContrac was impossible since context.Generator.ContractResolver.ResolveContract(context.ObjectType); was returning JsonPrimitiveContract. I didn't want to spend too much time on resolving that therefore I went on with the code and tried to perform something like dbc did in this code:

foreach (var propertySchema in schema.Properties)
        {
            // Find the corresponding JsonProperty from the contract resolver.
            var jProperty = contract.Properties[propertySchema.Key];
            // Check to see if the member has HiddenAttribute set.
            if (jProperty.AttributeProvider.GetAttributes(typeof(HiddenAttribute), true).Any())
                // If so add "options": { "hidden": true }
                propertySchema.Value.ExtensionData["options"] = new JObject(new JProperty("hidden", true));
        }

Another problem occured as schema.Properties was empty in most of the cases. I noticed that this custom provider was not only called once but it was called for every property that was the part of my base class for the schema and once for the base class itself (basically this provider was called hundreds of times). So I ended up just creating the schema in my class and then applying the ExtensionData. Therefore my provider besides doing some other logic it has the CheckIsHidden(JSchemaTypeGenerationContext context, JSchema schema) method that does the job:

        public static void CheckIsHidden(JSchemaTypeGenerationContext context, JSchema schema)
        {
            var hiddenAttribute = context.MemberProperty?.AttributeProvider?.GetAttributes(true)
                ?.FirstOrDefault(a => a.GetType().Name == nameof(JsonConfigIgnoreAttribute));
            if (hiddenAttribute != null)
            {
                schema.ExtensionData["options"] = new JObject(new JProperty("hidden", true));
            }
        }

The comment definitely helped me with achieving this as I was mainly looking for this one specific line schema.ExtensionData["options"] = new JObject(new JProperty("hidden", true));. Thanks very much!

Upvotes: 0

dbc
dbc

Reputation: 116941

The keywords "options": { "hidden": true } or even "hidden": true do not appear to be among the validation keywords for the current JSON Schema specification -- or any earlier version as far as I can tell. The only keywords that seem related are readOnly and writeOnly. From the docs:

New in draft 7 The boolean keywords readOnly and writeOnly are typically used in an API context. readOnly indicates that a value should not be modified. It could be used to indicate that a PUT request that changes a value would result in a 400 Bad Request response. writeOnly indicates that a value may be set, but will remain hidden. In could be used to indicate you can set a value with a PUT request, but it would not be included when retrieving that record with a GET request.

{
  "title": "Match anything",
  "description": "This is a schema that matches anything.",
  "default": "Default value",
  "examples": [
    "Anything",
    4035
  ],
  "readOnly": true,
  "writeOnly": false 
}

Thus it would seem that "options": { "hidden": true } is some sort of custom or 3rd-party extension to the JSON Schema standard. Json.NET schema supports such custom validation keywords through the JSchema.ExtensionData property. To set your hidden option in this extension data during automatic schema generation, define the following JSchemaGenerationProvider:

[System.AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class HiddenAttribute : System.Attribute
{
}

public class HiddenOptionProvider : CustomizedProviderBase
{
    public override JSchema GetSchema(JSchemaTypeGenerationContext context)
    {
        var schema = base.GetSchema(context);
        // Get the JsonObjectContract for this type.
        var contract = (JsonObjectContract)context.Generator.ContractResolver.ResolveContract(context.ObjectType);
        foreach (var propertySchema in schema.Properties)
        {
            // Find the corresponding JsonProperty from the contract resolver.
            var jProperty = contract.Properties[propertySchema.Key];
            // Check to see if the member has HiddenAttribute set.
            if (jProperty.AttributeProvider.GetAttributes(typeof(HiddenAttribute), true).Any())
                // If so add "options": { "hidden": true }
                propertySchema.Value.ExtensionData["options"] = new JObject(new JProperty("hidden", true));
        }
        
        return schema;
    }

    public override bool CanGenerateSchema(JSchemaTypeGenerationContext context) =>
        base.CanGenerateSchema(context) && context.Generator.ContractResolver.ResolveContract(context.ObjectType) is JsonObjectContract;
}

public abstract class CustomizedProviderBase : JSchemaGenerationProvider
{
    // Base class that allows generation of a default schema which may then be subsequently customized. 
    // Note this class contains state information and so is not thread safe.
    readonly Stack<Type> currentTypes = new ();

    public override JSchema GetSchema(JSchemaTypeGenerationContext context)
    {
        if (CanGenerateSchema(context))
        {
            var currentType = context.ObjectType;
            try
            {
                currentTypes.Push(currentType);
                return context.Generator.Generate(currentType);
            }
            finally
            {
                currentTypes.Pop();
            }
        }
        else
            throw new NotImplementedException();
    }
    
    public override bool CanGenerateSchema(JSchemaTypeGenerationContext context) => 
        !currentTypes.TryPeek(out var t) || t != context.ObjectType;
}

Then define your Person type as follows:

[DisplayName("Person")]
public class Person
{
    [JsonProperty("name", Required = Required.DisallowNull)]
    [DefaultValue("Jeremy Dorn"), MinLength(4), System.ComponentModel.DescriptionAttribute("First and Last name")]
    [Hidden] // Your custom attribute
    public string Name { get; set; } = "Jeremy Dorn";
}

And generate a schema as follows:

var generator = new JSchemaGenerator();
generator.GenerationProviders.Add(new HiddenOptionProvider());
var schema = generator.Generate(typeof(Person));            

You will get the following schema, as required:

{
  "title": "Person",
  "type": "object",
  "properties": {
    "name": {
      "description": "First and Last name",
      "options": {
        "hidden": true
      },
      "type": "string",
      "default": "Jeremy Dorn",
      "minLength": 4
    }
  }
}

Demo fiddle here.

Upvotes: 1

gregsdennis
gregsdennis

Reputation: 8428

I don't know about Newtonsoft, but JsonSchema.Net.Generation can do this easily with the built-in [JsonIgnore] attribute. This schema library is built on top of System.Text.Json.

I apparently need to document that this specifically is supported, but here's the rest of the docs for the library. I do have a test (line 168) confirming that it works.

Upvotes: 0

Related Questions