【问题标题】:Replace $ref with custom attribute for specific types将 $ref 替换为特定类型的自定义属性
【发布时间】:2020-11-04 15:35:50
【问题描述】:

是否可以用自定义属性替换类型的$ref 属性,但仅限于架构中的特定类型?看来 JSON.NET Schema 库对于自定义模式处理来说是孤注一掷。

sample code 非常简单,但据我所知,一旦您向GenerationProviders 添加类型,该库就不会为更深入类层次结构的类型提供自动模式生成。 (我不能共享代码,这是一个锁定的银行系统,无法访问互联网。)

我们正在使用从 .NET C# 类的整个层次结构中生成的模式文件,以使用 Java 的 Jackson 库生成相应的 Java 类。一些类属性是无法从模式表示或生成的复杂类型,应该引用现有的类定义,而其余的类属性(它们中的大多数)应该使用普通的$ref 属性。

因此,默认情况下,C# 类层次结构可能会生成如下内容:

"AccountType": {
    "$ref": "#/definitions/AccountTypeCode"
    },

而杰克逊需要这个:

"AccountType": {
    "existingJavaType": "com.foo.bar.pojo.AccountTypeCode"
    },

现在我们正在用伪善的字符串替换对模式进行后处理,但这感觉像是一种可能受支持的用例。

【问题讨论】:

标签: json.net jsonschema


【解决方案1】:

您可以使用custom JSchemaGenerationProvider 生成这样的架构,但完全抑制内部"$ref" 属性的存在会带来额外的复杂性。

考虑以下简化示例:

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

public class AccountTypeCode { }

public class SomeOtherData { }

您想为此数据模型生成一个架构,其中AccountTypeCode 使用特定的现有JavaType,而其他所有内容都保持不变,如下所示:

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

作为为任何 POCO 类型子集注入现有 JavaType 架构的第一次尝试,子类 JSchemaGenerationProvider 并覆盖 GetSchema() 和 CanGenerateSchema(),如下所示:

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);
}

(通过覆盖CanGenerateSchema(),您可以回退到除指定类型之外的所有类型的默认模式生成。这在docs 中未显示,并且可能导致您认为 Json.NET 模式不支持混合默认模式和自定义架构。)

现在生成一个模式如下,为AccountTypeCode注入所需的existingJavaType值:

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));       

这会产生以下架构(演示小提琴 #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"
    }
  }
}

如您所见,我们已经完成了一半。已根据需要插入了现有JavaType,但是 AccountTypeCode 已被提取为本地定义而不是内联。虽然可以通过将JSchemaGenerator.SchemaLocationHandling 设置为SchemaLocationHandling.Inline 来内联所有 定义,但似乎没有办法强制内联特定类型。然而,快速阅读reference source 会发现引用仅针对对象、数组和字典合约生成。因此,创建一个返回AccountTypeCode 的其他合同的custom contract resolver,比如JsonStringContract,将强制内联。或者当然这也会修改生成的架构——但无论如何您已经手动生成架构,因此这样做是无害的。

将这两部分放在一起,以下工厂可以为包含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),
           };
}

然后按如下方式使用:

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));       

演示小提琴#2 here.

【讨论】:

    猜你喜欢
    • 2020-10-24
    • 1970-01-01
    • 2017-02-26
    • 1970-01-01
    • 1970-01-01
    • 2012-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多