JSON模式属性取决于先前属性的值

时间:2018-11-08 11:10:11

标签: json jsonschema

我希望能够编写JSON模式代码,该代码允许一个属性的值依赖于另一个属性的值。

更具体地说,我有两个问题A和B。只有当问题A有特定答案时,问题B的答案才能为空。如果问题A没有答案,那么问题B的值必须为空。

例如

A: Do you like cars? Yes/No
B: What is your favourite car?

仅当问题A的答案为“是”时才能回答问题B,否则必须将其保留为空。

经过一些研究,我发现了这个Stack Overflow线程,该线程描述了枚举以及回答该问题的if-then-else方法。枚举非常接近我的需要,定义如下:

{
  "type": "object",
  "properties": {
    "foo": { "enum": ["bar", "baz"] },
    "bar": { "type": "string" },
    "baz": { "type": "string" }
  },
  "anyOf": [
    {
      "properties": {
        "foo": { "enum": ["bar"] }
      },
      "required": ["bar"]
    },
    {
      "properties": {
        "foo": { "enum": ["baz"] }
      },
      "required": ["baz"]
    }
  ]
}

在上面,当Foo的值为"Bar"时,则需要Bar属性。同样,值为"Baz"。但是,我不需要将属性设置为必需,而是希望能够将属性的类型从null更改为string。或做一些事情使B的答案有效。

对此有何想法?

1 个答案:

答案 0 :(得分:2)

您考虑了

  1. 未定义 B型预付款
  2. 在架构中使用“ dependencies”关键字,或针对问题A做出包含答案“是”的适当定义
  3. 仅由于这种依赖性而定义问题B类型?

让我们来总结一下

"questionA": {
  "type": "object",
  "properties": {
    "answer": {
      "type": "string",
      "minLength": 1,
      "enum": ["Yes", "No"]
    }
  }
}

"questionB": {
  "type": "object",
  "properties": {
    "answer": {
      "type": null,
    }

  }
}


"questions": {
          "type": "object",
          "properties": {
            "A": {"$ref": "#/definitions/questionA"},
            "B": {"$ref": "#/definitions/questionB"}
          },
          "if": {
            "properties" : {
              "A": {"enum": ["Yes"]}
            }
          },
          "then": {
            "B": //Type = string and min length = 1 <-- Unsure what to put here to change the type of QuestionB
          }

如果我正确理解您的问题,那么您想要达到的效果是:

如果受访者喜欢汽车,请向他询问最喜欢的汽车并获取答案,否则不要打扰最喜欢的汽车(最好将答案设为空)。

正如Relequestual在他的评论中正确指出的那样,JSON Schema使其很难“重新定义”类型。此外,每个if-then-else内容本身都必须是有效的架构。

为了达到此效果,您可能需要考虑以下方法:

  1. 像您一样将问题A定义为枚举
  2. 为问题B保留属性未预先定义
  3. 定义两个可能的模式,它们可以用作问题B的属性定义,并且可以作为依赖项的结果使用
  4. 根据问题A的值使用适当的问题B定义

下面列出了一些解决您的情况的示例架构(兼容draft07)。架构下方还提供了一些说明。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type" : "object",
  "propertyNames" : {
    "enum" : [
      "questionA",
      "questionB",
    ]
  },
  "properties" : {
    "questionA" : { "$ref" : "#/questionA" },
    "questionB" : { "$ref" : "#/questionB" },
  },
  "dependencies" : {
    "questionA" : {
      "$ref" : "#/definitions/valid-combinations-of-qA-qB"
    }
  },
  "definitions" : {
    "does-like-cars":{
      "properties" : {
        "questionA" : {
          "properties" : {
            "answer" : { "enum" : ["Yes","y"] }
          }
        },
        "questionB" : {
          "properties" : {
            "answer" : { 
              "$comment" : "Here #/questionB/answer becomes a type:string...",
              "$ref" : "#/questionB/definitions/answer-def/string"
            }
          }
        }
      },
      "required" : ["questionB"]
    },
    "doesnt-like-cars" :{
      "properties" : {
        "questionA" : {
          "properties" : {
            "answer" : { "enum" : ["No","n"] }
          }
        },
        "questionB" : {
          "properties" : {
            "answer" : { 
              "$comment" : "Here #/questionB/answer becomes a type:null...",
              "$ref" : "#/questionB/definitions/answer-def/null" 
            }
          }
        }
      }
    },
    "valid-combinations-of-qA-qB" : {
      "anyOf" : [
        { "$ref" : "#/definitions/doesnt-like-cars" },
        { "$ref" : "#/definitions/does-like-cars" }
      ]
    },
  },
  "examples" : [
    {
      "questionA" : {
        "answer" : "Yes",
      },
      "questionB" : {
        "answer" : "Ass-kicking roadster",
      },
    },
    {
      "questionA" : {
        "answer" : "No",
      },
      "questionB" : {
        "answer" : null,
      },
    },
    {
    },
  ],
  "questionA" : {
    "$id" : "#/questionA",
    "type" : "object",
    "propertyNames" : {
      "enum" : ["answer"]
    },
    "properties" : {
      "answer" : {"$ref" : "#/questionA/definitions/answer-def"}
    },
    "definitions" : {
      "answer-def" : {
        "$comment"  : "Consider using pattern instead of enum if case insensitiveness is required",
        "type" : "string",
        "enum" : ["Yes", "y", "No", "n"]
      }
    }
  },
  "questionB" : {
    "$id" : "#/questionB",
    "$comment" : "Please note no properties definitions here aside from propertyNames",
    "type" : "object",
    "propertyNames" : {
      "enum" : ["answer"]
    },
    "definitions" : {
      "answer-def" : {
        "string" : {
          "type" : "string",
          "minLength" : 1,
        },
        "null" : {
          "type" : "null"
        }
      }
    }
  },
}

为什么这么复杂?

因为您的要旨是这样;-)更严重的是,这是因为:

  • 在要点中,您将两个问题都定义为对象。可能有 它背后的正当理由,所以我一直保持这种方式(但是无论何时平放 可以使用属性列表,例如“ questionA-answer”, 我希望使用“ questionB-answer”,这样可以减少架构规则的嵌套, 因此更具可读性且易于创建)。

  • 从您的问题和要点看来,这对您很重要 “ questionB / answer”为null而不是未通过验证 反对/忽略不相关的内容,因此我保持了

一步一步

作为对象的问题

请注意,我已经为“ questionA”和“ questionB”创建了单独的子模式。这是我的个人喜好,没有什么可以阻止您将所有内容都放入主模式的“ definitions”模式中,但是我通常这样做是因为:

  • 在使一切都按其应有的方式工作之后,将大型模式拆分为多个较小的文件会更容易(鼓励重用子模式,如果有人在我的模式之后想到建立其数据模型,则可以帮助以编程语言构造模型)
  • 保留对象模式/子方案的正确封装并保持良好状态,相对引用通常对读者也更清楚
  • 有助于在处理JSON语法的编辑器中查看复杂的模式

“属性名称”

因为我们在这里处理“ type”:“ object”,所以我使用了“ propertyNames”关键字来定义允许的属性名称的架构(因为编程语言中的类通常具有静态属性集)。尝试在每个对象中输入此集合之外的属性-模式验证失败。这样可以防止对象中出现垃圾。如果不需要这种行为,只需从每个对象中删除“ propertyNames”模式。

“ questionB”-更改类型的花样在哪里?

诀窍是:不要预先定义属性类型和其他相关的架构规则。请注意,“ questionB”模式中没有“属性”模式。相反,我使用“ definitions”为“ questionB”对象中的“ answer”属性准备了两种可能的定义。我将根据“ questionA”答案值来使用它们。

“示例”部分?

一些对象,应该说明架构的工作方式。试一试答案值,属性的存在等。请注意,空对象也将通过验证,因为不需要属性(如您的要点),并且只有一个依赖项-如果出现“ questionA”,则显示“ questionB”必须也出现。

好的,好的,请从上到下

好的。 因此,主模式可以具有两个属性:

  • questionA(一个包含属性“ answer”的对象)

  • questionB(包含属性“ answer”的对象)

是否需要“#/ questionA”? ->不,至少要根据您的要旨。

是否需要“ questionB”? ->仅当出现“#/ questionA”时。要增加侮辱性伤害,请:-)“#/ questionB / answer”的类型和允许的值严格取决于“#/ questionA / answer”的值。

->我可以安全地预定义主要对象,问题对象的基础,并且需要定义依赖项

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type" : "object",
  "propertyNames" : {
    "enum" : [
      "questionA",
      "questionB",
    ]
  },
  "properties" : {
    "questionA" : { "$ref" : "#/questionA" },
    "questionB" : { "$ref" : "#/questionB" },
  },
  "dependencies" : {
    "questionA" : {
      "$comment" : "when questionA prop appears in validated entity, do something to enforce questionB to be what it wants to be! (like Lady Gaga in Machette...)"
    }
  },
  "questionA" : {
    "$id" : "#/questionA",
    "type" : "object",
    "propertyNames" : {
      "enum" : ["answer"]
    },
  },
  "questionB" : {
    "$id" : "#/questionB",
    "type" : "object",
    "propertyNames" : {
      "enum" : ["answer"]
    },
  },
}

请注意:我正在有意识地通过“ $ id”关键字为问题子方案设置相对基本引用,以便能够将架构拆分为多个较小的文件,并具有可读性。

->我可以安全地预定义“ questionA / answer”属性:类型,允许的值等。

"questionA" : {
    "$id" : "#/questionA",
    "type" : "object",
    "propertyNames" : {
      "enum" : ["answer"]
    },
    "properties" : {
      "answer" : {"$ref" : "#/questionA/definitions/answer-def"}
    },
    "definitions" : {
      "answer-def" : {
        "$comment"  : "Consider using pattern instead of enum if case insensitiveness is required",
        "type" : "string",
        "enum" : ["Yes", "y", "No", "n"]
      }
    }
  },

注意::我使用“定义”来定义特定属性的架构。以防万一我需要在其他地方重用该定义...(是的,对我的偏执)

->我不能如上所述安全地预定义“#/ questionB / answer”属性,必须在“#/ questionB”子模式中执行“技巧”部分

"questionB" : {
    "$id" : "#/questionB",
    "$comment" : "Please note no properties definitions here aside from propertyNames",
    "type" : "object",
    "propertyNames" : {
      "enum" : ["answer"]
    },
    "definitions" : {
      "answer-def" : {
        "string" : {
          "type" : "string",
            "minLength" : 1,
        },
        "null" : {
          "type" : "null"
        }
      }
    }
  },

注意:是否看到“#/ definitions / answer-def”?有两个子节点:“#/ definitions / answer-def / string”和“#/ definitions / answer-def / null”。我目前尚不确定该怎么做,但我知道我绝对需要最后使用“#/ questionB / answer”属性模式进行杂耍的功能。

->我必须为两个答案的有效组合定义规则,因为“#/ questionB / answer”必须始终存在;我正在主模式中执行此操作,该模式使用问题子模式,因为这是对它们的限制,从逻辑上讲,它是定义此类规则的好地方。

"definitions" : {
    "does-like-cars":{
      "properties" : {
        "questionA" : {
          "properties" : {
            "answer" : { "enum" : ["Yes","y"] }
          }
        },
        "questionB" : {
          "properties" : {
            "answer" : { 
              "$comment" : "Here #/questionB/answer becomes a type:string...",
              "$ref" : "#/questionB/definitions/answer-def/string"
            }
          }
        }
      },
      "required" : ["questionB"]
    },
    "doesnt-like-cars" :{
      "properties" : {
        "questionA" : {
          "properties" : {
            "answer" : { "enum" : ["No","n"] }
          }
        },
        "questionB" : {
          "properties" : {
            "answer" : { 
              "$comment" : "Here #/questionB/answer becomes a type:null...",
              "$ref" : "#/questionB/definitions/answer-def/null" 
            }
          }
        }
      }
    },
    "valid-combinations-of-qA-qB" : {
      "anyOf" : [
        { "$ref" : "#/definitions/doesnt-like-cars" },
        { "$ref" : "#/definitions/does-like-cars" }
      ]
    },

所以有些人喜欢汽车-我基本上定义了“#/ questionA / answer”的允许值和“#/ questionB / answer”的属性的相关定义。由于这是架构,因此两个集合必须匹配才能满足此定义。请注意,为了不验证针对模式仅包含“ questionA”属性键的JSON,我已按要求将“ questionB”属性键标记为必需。

我为那些不喜欢汽车的人做过类似的事情(怎么可能不喜欢汽车?邪恶的时代...),最后我在“ valid-combinations-qa-qB”中说:要么,要么人。您要么喜欢汽车并给我答案,要么您不喜欢汽车并且答案必须为空。 “ XOR”(“ oneOf”)会自动出现,但是由于我已经定义了 like汽车AND answer 不喜欢cars AND answer = null 作为完整模式,逻辑OR完全足够->“ anyOf”。

最后,画龙点睛的是在主架构的“依赖项”部分中使用该规则,该规则翻译为:如果“ questionA”出现在经过验证的实例中,则...或...

"dependencies" : {
    "questionA" : {
      "$ref" : "#/definitions/valid-combinations-of-qA-qB"
    }
  },

希望它可以澄清您的情况并为您提供帮助。

悬而未决的问题

为什么不使用对象“答案”具有反映每个问题答案的属性,并使用键来标识问题?就答案之间的依赖性而言,它可以简化一些规则和参考(较少键入,是的,我是个懒惰的家伙)。

为什么“#/ questionB /答案”必须为空,而不是如果“#/ questionA / answer”为:{“枚举”:[“否”]}为空?

推荐阅读

请参阅“了解JSON模式”:https://json-schema.org/understanding-json-schema/index.html

一些基本示例:https://json-schema.org/learn/

JSON模式验证参考:https://json-schema.org/latest/json-schema-validation.html

大量StackOverflow问答对如何使用JSON模式管理不同案例提供了很好的见识。

有时,检查相对的JSON指针RFC也可能会有所帮助。