JSON架构:使用“allof”和“additionalProperties”。

84

假设我们有以下模式(来自这里的教程):

{
  "$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": {
      "allOf": [
        { "$ref": "#/definitions/address" },
        { "properties":
          { "type": { "enum": [ "residential", "business" ] } },
          "required": ["type"]
        }
      ]
    } 

  }
}

这里是一个有效的实例:

{
      "shipping_address": {
        "street_address": "1600 Pennsylvania Avenue NW",
        "city": "Washington",
        "state": "DC",
        "type": "business"
      }
}

我需要确保shipping_address的任何额外字段都是无效的。我知道为此存在additionalProperties,应该将其设置为"false"。但是当我像下面这样设置"additionalProprties":false时:

"shipping_address": {
          "allOf": [
            { "$ref": "#/definitions/address" },
            { "properties":
              { "type": { "enum": [ "residential", "business" ] } },
              "required": ["type"]
            }
          ],
          "additionalProperties":false
        } 

我遇到了一个验证错误(在这里检查过):

[ {
  "level" : "error",
  "schema" : {
    "loadingURI" : "#",
    "pointer" : "/properties/shipping_address"
  },
  "instance" : {
    "pointer" : "/shipping_address"
  },
  "domain" : "validation",
  "keyword" : "additionalProperties",
  "message" : "additional properties are not allowed",
  "unwanted" : [ "city", "state", "street_address", "type" ]
} ] 

问题是: 我应该如何仅限制shipping_address部分的字段? 预先感谢。

5个回答

108

[草案 v4 验证规范的作者在此]

您遇到了 JSON Schema 中最常见的问题,即其无法像用户期望的那样进行继承;但同时这也是其核心特性之一。

当您执行以下操作时:

"allOf": [ { "schema1": "here" }, { "schema2": "here" } ]

schema1schema2互不了解,它们在各自的上下文中进行评估。

在您所遇到的情况中,很多人都期望在schema1中定义的属性将为schema2所知; 但实际情况并非如此,并且永远不会如此。

这就是我为什么在草案v5中提出了这两个建议的原因:

您的shipping_address模式将如下所示:

{
    "merge": {
        "source": { "$ref": "#/definitions/address" },
        "with": {
            "properties": {
                "type": { "enum": [ "residential", "business" ] }
            }
        }
    }
}

address 中将 strictProperties 定义为 true


顺便说一下,我也是你提到的那个网站的作者。

现在,让我回到草案v3。草案v3确实定义了extends,其值可以是模式或模式数组。根据此关键字的定义,这意味着实例必须针对当前模式extends中指定的所有模式都有效;基本上,草案v4的allOf就是草案v3的extends

考虑以下示例(草案v3):

{
    "extends": { "type": "null" },
    "type": "string"
}

现在,那个:

{
    "allOf": [ { "type": "string" }, { "type": "null" } ]
}

它们是一样的。或者说呢?

{
    "anyOf": [ { "type": "string" }, { "type": "null" } ]
}

还是那个?

{
    "oneOf": [ { "type": "string" }, { "type": "null" } ]
}

总之,这意味着在草案v3中的extends从未真正做到人们期望的事情。使用草案v4,*Of关键字被明确定义。

但你遇到的问题是迄今为止最常见的问题。因此,我的建议将彻底解决这个误解的根源!


3
这两个属性不属于任何规范 - 它们是@fge正在提议的功能,因此不能保证它们会出现在任何未来版本中。 - cloudfeet
2
@fge - 在你的回答中,关键字不是规范的一部分,但是它们是你提出的扩展。它们的呈现方式使它们看起来像是官方解决方案,没有任何警告表明v5处于不稳定状态,我认为这是具有误导性的。 - cloudfeet
45
目前版本为v7,我没有看到任何mergestrictProperties。现在能否像OP所说的那样做? - Julian Honma
2
@JulianHonma 2019年1月,strictPropertiesmerge不是规范草案v7的一部分。 - Sebastian Barth
8
自2019年9月起,可以将"unevaluatedProperties"设置为false。这将最终解决此处的问题。 - Andreas H.
显示剩余7条评论

14

additionalProperties 适用于所有在 直接模式 中未被 propertiespatternProperties 覆盖的属性。

这意味着当你有:

    {
      "allOf": [
        { "$ref": "#/definitions/address" },
        { "properties":
          { "type": { "enum": [ "residential", "business" ] } },
          "required": ["type"]
        }
      ],
      "additionalProperties":false
    }

additionalProperties 在这里适用于所有属性,因为没有同级的properties条目——allOf中的那个不算。

你可以做的一件事是将properties定义上移一级,并为导入的属性提供存根条目:

    {
      "allOf": [{"$ref": "#/definitions/address"}],
      "properties": {
        "type": {"enum": ["residential", "business"]},
        "addressProp1": {},
        "addressProp2": {},
        ...
      },
      "required": ["type"],
      "additionalProperties":false
    }

这意味着 additionalProperties 不会应用于您想要的属性。


谢谢回复,但是没有帮助:这里有一个错误:[{ “level”:“error”, “schema”:{ “loadingURI”:“#”, “pointer”:“/ properties / shipping_address” }, “instance”:{ “pointer”:“/ shipping_address” }, “domain”:“validation”, “keyword”:“additionalProperties”, “message”:“不允许使用其他属性”, “unwanted”:[“city”,“state”,“street_address”] }] - 与之前相同。 - lm.
你是否像我例子中的"addressProp1"一样,导入了"city"、"state"和"street_address"? - cloudfeet
你能澄清一下吗?我根据你的更改修改了模式。 "city","state"和"street_address"仍然是引用模式的一部分,你是这个意思吗?我已经使用验证器http://json-schema-validator.herokuapp.com/ 进行了检查。 - lm.
1
这是目前唯一可行的解决方案,尽管有些繁琐(当你不得不添加十几个属性只为了让“additionalProperties:false”满意时)。 - Julian Honma
无法工作:/。 - John T

8
这是Yves-M的解决方案的稍微简化版本:
{
  "$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": {
      "allOf": [
        {
          "$ref": "#/definitions/address"
        }
      ],
      "properties": {
        "type": {
          "enum": [
            "residential",
            "business"
          ]
        },
        "street_address": {},
        "city": {},
        "state": {}
      },
      "required": [
        "type"
      ],
      "additionalProperties": false
    }
  }
}

这样可以保留基本address模式中所需属性的验证,并仅在shipping_address中添加所需的type属性。

很遗憾,additionalProperties仅考虑相邻级别的属性。也许有理由这样做。但这就是为什么我们需要重复继承的属性。

在此处,我们使用空对象语法以简化形式重复继承的属性。这意味着具有这些名称的属性将始终有效,无论其包含何种值。但我们可以依靠allOf关键字来强制执行在基本address模式中声明的类型约束(以及任何其他约束)。


1
这是我读过的最好的例子之一,它解释了整个情况。谢谢@ted epstein - Brett
谢谢,@Brett!很高兴你觉得有帮助。 - Ted Epstein

5

由于还没有人发布有效的答案针对2019-09及以上的规范,而我差点错过了Andreas H.的评论;

{
  "$schema": "http://json-schema.org/2019-09/schema#",

  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city":           { "type": "string" },
        "state":          { "type": "string" }
      },
      "required": ["street_address", "city", "state"]
      // additionalProperties: false    // <-- Remove completely if present 
    }
  },

  "type": "object",

  "properties": {
    "billing_address": { "$ref": "#/definitions/address" },
    "shipping_address": {
      "unevaluatedProperties": false,   // <-- Add to same level as allOf as false
      "allOf": [
        { "$ref": "#/definitions/address" },
        { "properties":
          { "type": { "enum": [ "residential", "business" ] } },
          "required": ["type"]
        }
      ]
    } 
  }
}

作者在这里提供了非常清晰简明的解释


这是正确的想法,但代码无效,因为unevaluatedProperties仅在2019-09或更新的草案中有效。此答案应更新以指示适当的$schema版本。 - Jeremy Fiel

1

不要在定义层面上设置additionalProperties=false

只要遵循这个原则就可以保证一切顺利:

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

    "type": "object",
    "properties": {

        "billing_address": {
            "allOf": [
                { "$ref": "#/definitions/address" }
            ],
            "properties": {
                "street_address": {},
                "city": {},
                "state": {}                 
            },          
            "additionalProperties": false
            "required": ["street_address", "city", "state"] 
        },

        "shipping_address": {
            "allOf": [
                { "$ref": "#/definitions/address" },
                {
                    "properties": {
                        "type": {
                            "enum": ["residential","business"]
                        }
                    }
                }
            ],
            "properties": {
                "street_address": {},
                "city": {},
                "state": {},
                "type": {}                          
            },              
            "additionalProperties": false
            "required": ["street_address","city","state","type"] 
        }

    }
}

您的每个billing_addressshipping_address都应指定其自己的必需属性。

如果要将其属性与其他属性组合,则不应在您的定义中使用"additionalProperties": false


1
如果我理解正确的话,billing_address的定义通过引用street_address、city和state属性;然后在本地再次定义它们。是这样吗?shipping_address似乎做了类似的事情。它看起来是多余的,而我使用$ref的目标是避免重复定义。请告诉我,这样做有什么作用? - chrisinmtown
为了在required中使用它们,您必须再次在properties中指定street_addresscitystate,这就是为什么它们带有{} - Yves M.
“你必须在属性中重新指定街道地址、城市和州,以便在所需时使用它们,这就是它们带有{}的原因。” 不需要这样做。至少根据规范,所需和属性是独立的。 - spinkus
10
这明显违反了DRY的原则,这也是OP提出这个问题的原因。 - LionC
我同意这违反了DRY原则。除此之外,它会产生不易读的代码,也不直观。我不确定为什么additionalProperties只检查允许属性的同级别,但在我看来,这应该被改变。 - omni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接