replace ref with the custom attribute for specific types

You can generate such a schema using a custom JSchemaGenerationProvider, though there is an extra complication involved in completely suppressing the presence of internal "$ref" properties.

Consider the following simplified example:

public class Account
{
    public AccountTypeCode AccountType { get; set; }
    public SomeOtherData SomeOtherData { get; set; }
}

public class AccountTypeCode { }

public class SomeOtherData { }

You would like to generate a schema for this data model in which AccountTypeCode uses a specific existingJavaType, and everything else is unchanged, like so:

{
  "definitions": {
    "SomeOtherData": {
      "type": [
        "object",
        "null"
      ]
    }
  },
  "type": "object",
  "properties": {
    "AccountType": {
      "existingJavaType": "com.foo.bar.pojo.AccountTypeCode"
    },
    "SomeOtherData": {
      "$ref": "#/definitions/SomeOtherData"
    }
  }
}

As a first attempt at injecting an existingJavaType schema for any subset of POCO types, subclass JSchemaGenerationProvider and override GetSchema() and CanGenerateSchema() as follows:

class ExistingJavaTypeSchemaProvider : JSchemaGenerationProvider
{
    readonly Dictionary<Type, string> existingJavaTypes;

    public ExistingJavaTypeSchemaProvider(Dictionary<Type, string> existingJavaTypes) 
        => this.existingJavaTypes = existingJavaTypes ?? throw new ArgumentNullException(nameof(existingJavaTypes));

    public override JSchema GetSchema(JSchemaTypeGenerationContext context)
        => existingJavaTypes.TryGetValue(context.ObjectType, out var existingJavaType) ? JSchema.Parse(new JObject(new JProperty("existingJavaType", existingJavaType)).ToString()) : null;

    public override bool CanGenerateSchema(JSchemaTypeGenerationContext context) 
        => existingJavaTypes.ContainsKey(context.ObjectType);
}

(By overriding CanGenerateSchema() you can fall back to default schema generation for all but the specified types. This is not shown in the docs and may have lead you into thinking Json.NET Schema does not support mixing of default and custom schemas.)

Now generate a schema as follows, injecting the required existingJavaType value for AccountTypeCode:

var generator =  new JSchemaGenerator
{
    GenerationProviders = { new ExistingJavaTypeSchemaProvider(new Dictionary<Type, string>
                                                               {{ typeof(AccountTypeCode), "com.foo.bar.pojo.AccountTypeCode" }}
                                                              ) },
};
generator.DefaultRequired = Required.Default;
var schema = generator.Generate(typeof(Account));       

This results in the following schema (demo fiddle #1 here):

{
  "definitions": {
    "AccountTypeCode": {
      "existingJavaType": "com.foo.bar.pojo.AccountTypeCode"
    },
    "SomeOtherData": {
      "type": [
        "object",
        "null"
      ]
    }
  },
  "type": "object",
  "properties": {
    "AccountType": {
      "$ref": "#/definitions/AccountTypeCode"
    },
    "SomeOtherData": {
      "$ref": "#/definitions/SomeOtherData"
    }
  }
}

As you can see, we’re halfway there. The existingJavaType has been inserted as required, however AccountTypeCode has been extracted as a local definition rather than inlined. While it is possible to inline all definitions by setting JSchemaGenerator.SchemaLocationHandling to SchemaLocationHandling.Inline, there does not appear to be a way to force a particular type to be inlined. However, a quick perusal of the reference source shows that references are only generated for object, array and dictionary contracts. Thus creating a custom contract resolver that returns some other contract for AccountTypeCode, say JsonStringContract, would force inlining. Or course this would also modify the schema generated — but you’re already manually generating the schema anyway, thus doing so is harmless.

Putting the two pieces together, the following factory can manufacture a schema generator for types that contain AccountTypeCode:

public static class ExistingJavaTypeJSchemaGeneratorFactory
{
    class ExistingJavaTypeSchemaProvider : JSchemaGenerationProvider
    {
        readonly Dictionary<Type, string> existingJavaTypes;
        
        public ExistingJavaTypeSchemaProvider(Dictionary<Type, string> existingJavaTypes) 
            => this.existingJavaTypes = existingJavaTypes ?? throw new ArgumentNullException(nameof(existingJavaTypes));
        
        public override JSchema GetSchema(JSchemaTypeGenerationContext context)
            => existingJavaTypes.TryGetValue(context.ObjectType, out var existingJavaType) ? JSchema.Parse(new JObject(new JProperty("existingJavaType", existingJavaType)).ToString()) : null;

        public override bool CanGenerateSchema(JSchemaTypeGenerationContext context) 
            => existingJavaTypes.ContainsKey(context.ObjectType);
    }

    class ExistingJavaTypeContractResolver : DefaultContractResolver
    {
        readonly Dictionary<Type, string> existingJavaTypes;
        
        public ExistingJavaTypeContractResolver(Dictionary<Type, string> existingJavaTypes) 
            => this.existingJavaTypes = existingJavaTypes ?? throw new ArgumentNullException(nameof(existingJavaTypes));

        protected override JsonContract CreateContract(Type objectType) 
            => existingJavaTypes.ContainsKey(objectType) ? new JsonStringContract(objectType) : base.CreateContract(objectType);
    }
    
    public static JSchemaGenerator CreateJSchemaGenerator(Dictionary<Type, string> existingJavaTypes)
        => new JSchemaGenerator
           {
               GenerationProviders = { new ExistingJavaTypeSchemaProvider(existingJavaTypes) },
               ContractResolver = new ExistingJavaTypeContractResolver(existingJavaTypes),
           };
}

And then use it as follows:

var generator = ExistingJavaTypeJSchemaGeneratorFactory.CreateJSchemaGenerator(
    new Dictionary<Type, string>
    {
        {typeof(AccountTypeCode), "com.foo.bar.pojo.AccountTypeCode"}
    });
generator.DefaultRequired = Required.Default;
var schema = generator.Generate(typeof(Account));       

Demo fiddle #2 here.

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top