Схема JSON: "allof" с "дополнительными свойствами"
Предположим, что у нас есть схема, следующая за схемой (из учебника здесь):
{
"$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
? Спасибо заранее.
Ответы
Ответ 1
[автор проекта спецификации проверки v4 здесь]
Вы столкнулись с наиболее распространенной проблемой в схеме JSON, т.е. с ее фундаментальной неспособностью выполнять наследование по мере того, как ожидают пользователи; но в то же время это одна из основных функций.
Когда вы выполните:
"allOf": [ { "schema1": "here" }, { "schema2": "here" } ]
schema1
и schema2
не знают друг друга; они оцениваются в их собственном контексте.
В вашем сценарии, с которым сталкивается много людей, вы ожидаете, что свойства, определенные в schema1
, будут известны schema2
; но это не так и никогда не будет.
Эта проблема заключается в том, почему я сделал эти два предложения для проекта v5:
Ваша схема для shipping_address
будет следующей:
{
"merge": {
"source": { "$ref": "#/definitions/address" },
"with": {
"properties": {
"type": { "enum": [ "residential", "business" ] }
}
}
}
}
вместе с определением strictProperties
- true
в address
.
Кстати, я также являюсь автором сайта, на который вы ссылаетесь.
Теперь позвольте мне вернуться к проекту 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" } ]
}
В общем, это означает, что extends
в проекте v3 никогда не делал то, что люди ожидали от него. С черновиками v4 четко определены ключевые слова *Of
.
Но проблема, с которой вы столкнулись, является наиболее часто встречающейся проблемой, безусловно. Отсюда мои предложения, которые раз и навсегда угасят этот источник непонимания!
Ответ 2
additionalProperties
применяется ко всем свойствам, которые не учитываются в properties
или patternProperties
в непосредственной схеме.
Это означает, что когда у вас есть:
{
"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
не будет применяться к свойствам, которые вы хотите.
Ответ 3
Здесь немного упрощенная версия Yves-M Solution:
{
"$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
и просто добавляет требуемое свойство type
в shipping_address
.
Несчастливо, что additionalProperties
учитывает только непосредственные свойства уровня брата. Может быть, есть причина для этого. Но именно поэтому нам нужно повторить унаследованные свойства.
Здесь мы повторяем унаследованные свойства в упрощенной форме, используя синтаксис пустого объекта. Это означает, что свойства с этими именами будут действительны независимо от того, какую ценность они содержат. Но мы можем полагаться на ключевое слово allOf
для обеспечения ограничений типа (и любых других ограничений), объявленных в базовой схеме address
.
Ответ 4
Не устанавливайте addProperties = 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_address
и shipping_address
должен указывать свои собственные требуемые свойства.
У вашего определения не должно быть "additionalProperties": false
, если вы хотите совместить его свойства с другими.