Reputation: 33
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
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
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
andwriteOnly
are typically used in an API context.readOnly
indicates that a value should not be modified. It could be used to indicate that aPUT
request that changes a value would result in a400 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 aPUT
request, but it would not be included when retrieving that record with aGET
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
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