如何在代码中定义包含定义的 Json 模式

How do I define a Json Schema containing definitions, in code

我正在尝试通过使用 Newtonsoft.Json.Schema:

在代码中定义模式来复制以下 Json Schema 示例
{
  "$schema": "http://json-schema.org/draft-04/schema#",

  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city":           { "type": "string" },
        "state":          { "type": "string" }
      },
      "required": ["street_address", "city", "state"]
    }
  },

  "type": "object",

  "properties": {
    "billing_address": { "$ref": "#/definitions/address" },
    "shipping_address": { "$ref": "#/definitions/address" }
  }

这是我目前为止最接近的结果。 (示例在 F# 中,但也可能在 C# 中。)

代码:

open Newtonsoft.Json.Schema
open Newtonsoft.Json.Linq

let makeSchema = 
    let addressSchema = JSchema()
    addressSchema.Properties.Add("street_address", JSchema(Type = Nullable(JSchemaType.String)))
    addressSchema.Properties.Add("city", JSchema(Type = Nullable(JSchemaType.String)))
    addressSchema.Properties.Add("state", JSchema(Type = Nullable(JSchemaType.String)))
    addressSchema.Required.Add "street_address"
    addressSchema.Required.Add "city"
    addressSchema.Required.Add "state"

    let schema = JSchema()
    schema.Properties.Add("billing_address", addressSchema)
    schema.Properties.Add("shipping_address", addressSchema)
    schema

输出:

{
  "properties": {
    "billing_address": {
      "properties": {
        "street_address": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "state": {
          "type": "string"
        }
      },
      "required": [
        "street_address",
        "city",
        "state"
      ]
    },
    "shipping_address": {
      "$ref": "#/properties/billing_address"
    }
  }
}

如您所见,两个地址中只有一个是使用对另一个模式的引用定义的,地址模式在 "properties" 而不是 "definitions" 中。在 "definitions" 中定义模式并在其他地方引用它有什么诀窍?

黑客节! :-)

根据 source code,JSON.NET 架构不会写 definitions 属性,故事结束。所以都无望了……差不多了。

它确实在另一个地方使用 definitions 属性。即 - when generating schema from a type。在此过程中,它会创建一个 JObject,将所有模式推送到其中,然后将该对象添加到 definitions 键下的 JSchema.ExtensionData。当从另一个地方引用模式时,模式编写者将尊重 definitions 对象(如果存在),从而使整个事物协同工作。

所以,有了这些知识,我们就可以破解它了:

let makeSchema = 
    let addressSchema = JSchema()
    ...

    let definitions = JObject() :> JToken
    definitions.["address"] <- addressSchema |> JSchema.op_Implicit

    let schema = JSchema()
    schema.ExtensionData.["definitions"] <- definitions
    schema.Properties.Add("billing_address", addressSchema)
    schema.Properties.Add("shipping_address", addressSchema)
    schema

瞧!生成的模式现在有一个 definitions 对象,正如神圣的文本告诉我们它应该:

{
  "definitions": {
    "address": {
      "properties": {
        "street_address": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "state": {
          "type": "string"
        }
      },
      "required": [
        "street_address",
        "city",
        "state"
      ]
    }
  },
  "properties": {
    "billing_address": {
      "$ref": "#/definitions/address"
    },
    "shipping_address": {
      "$ref": "#/definitions/address"
    }
  }
}

几点说明:

  1. 从 JSON.NET 的角度来看,definitions 的名字并不特别。如果您将行 schema.ExtensionData.["definitions"] 更改为不同的内容,比如 schema.ExtensionData.["xyz"],它仍然有效,所有引用都指向 "#/xyz/address".
  2. 这整个机制显然是一个 hack 显然不是,according to James Netwon-King。关键的见解似乎是 JsonSchemaWriter 将能够查找之前提到的任何模式并在其他地方使用对它们的引用。这允许人们将模式推到任何喜欢的地方并期望它们被引用。
  3. 那个op_Implicit的电话进来是有必要的。 JSchema 不是 JToken 的子类型,所以你不能像那样把它塞进 definitions.["address"],你必须先把它转换成 JToken。幸运的是,有一个 implicit cast operator defined for that. Unfortunately, it's not straightforward, there seems to be some magic going on. This happens transparently in C#(因为,你知道的,没有足够的混淆),但在 F# 中你必须明确地调用它。