Свойство JSON Schema зависит от значения предыдущего свойства - PullRequest
0 голосов
/ 08 ноября 2018

Я хотел бы иметь возможность написать код схемы JSON, который позволяет значению одного свойства зависеть от значения другого свойства.

В частности, у меня есть два вопроса A и B. Ответ на вопрос B может быть не нулевым, если на вопрос A есть конкретный ответ. Если вопрос A не имеет такого ответа, тогда значение вопроса B должно быть нулевым.

например.

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

На вопрос B можно ответить только в том случае, если ответом на вопрос A является «Да», в противном случае его следует оставить пустым.

После некоторого исследования я нашел этот поток переполнения стека, который описывает перечисление и подходы 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". Однако вместо того, чтобы сделать свойство обязательным, я хочу иметь возможность изменить тип свойства с нуля на строку. Или сделайте что-нибудь, чтобы сделать ответ B действительным.

Есть мысли по этому поводу?

1 Ответ

0 голосов
/ 15 ноября 2018

Рассматривали ли вы

  1. не определяет тип B авансом
  2. с использованием ключевого слова "зависимости" для вашей схемы или соответствующего определения, содержащего ответ "Да" на вопрос А
  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 как enum, как вы это сделали
  2. Оставить свойство для вопроса B неопределенным заранее
  3. Определите две возможные схемы, которые могут работать как определение свойства для questionB и могут использоваться в результате зависимости
  4. Используйте правильное определение вопроса B относительно значения вопроса A

Ниже приведен пример схемы (совместимой с 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» является нулевым, а не проверенным против / игнорируется, когда это не актуально, поэтому я сохранил это так

Шаг за шагом

Вопросы как объекты

Обратите внимание, что я создал отдельные подсхемы для "questionA" и "questionB". Это мое личное предпочтение, и ничто не мешает вам получить все внутри схемы «определений» основной схемы, однако я обычно делаю это так, потому что:

  • легче разбить большую схему на несколько файлов меньшего размера после того, как вы заставите все работать как надо (поощряет повторное использование подсхем и помогает структурировать модели в языках программирования, если кто-то получит идею построить свою модель данных после моей схемы )
  • обеспечивает правильную инкапсуляцию схем / подсхем объектов, а относительная ссылка обычно более понятна читателю
  • помогает просматривать сложные схемы в редакторах, которые обрабатывают синтаксис JSON

"propertyNames"

Поскольку мы работаем здесь над «type»: «object», я использовал ключевое слово «propertyNames», чтобы определить схему для разрешенных имен свойств (поскольку классы в языках программирования обычно имеют статические наборы свойств). Попробуйте ввести в каждый объект свойство вне этого набора - проверка схемы не удалась. Это предотвращает мусор в ваших объектах. Если это нежелательное поведение, просто удалите схемы «propertyNames» из каждого объекта.

"questionB" - где находится трюк с изменением типа?

Хитрость в том, что не определяет тип свойства и другие соответствующие правила схемы заранее . Обратите внимание, что в схеме «questionB» отсутствует схема «properties». Вместо этого я использовал «определения», чтобы подготовить два возможных определения свойства «ответ» внутри объекта «вопрос В». Я буду использовать их в зависимости от значения ответа «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". Поскольку это схема, оба набора должны совпадать, чтобы соответствовать этому определению. Обратите внимание, что я пометил ключ свойства «questionB» как необходимый, чтобы не проверять JSON, который содержит только ключ свойства «questionA» для схемы.

Я сделал то же самое для тех, кто не любит машины (как нельзя любить машины ?! Злые времена ...), и в конце я сказал в "valid-комбинации-of-qA-qB": или люди. Либо вы любите машины и даете мне ответ, либо вам не нравятся машины, и ответ должен быть нулевым. «XOR» («oneOf») приходит на ум автоматически, но так как я определил как автомобили И отвечаю и не люблю автомобили И отвечаю = ноль как полные схемы, логично ИЛИ вполне достаточно -> "anyOf".

В конце завершающим штрихом стало использование этого правила в разделе «зависимости» основной схемы, что означает: если в проверенном экземпляре появляется «questionA», то ... или ...

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

Надеюсь, это прояснит и поможет в вашем случае.

Открытые вопросы

Почему бы не использовать "ответы" объекта со свойствами, отражающими каждый ответ на вопрос, с ключом, идентифицирующим вопрос? Это может немного упростить правила и ссылки в отношении зависимостей между ответами (меньше печатать, да, я ленивый парень).

Почему "# / questionB / answer" должно быть нулевым, а просто игнорировать его, если "# / questionA / answer": {"enum": ["No"]}?

Рекомендуемое чтение

См. «Понимание схемы JSON»: https://json -schema.org / понимание-json-схема / index.html

Некоторые основные примеры: https://json -schema.org / learn /

Ссылка для проверки схемы JSON: https://json -schema.org / latest / json-schema-validation.html

Многие вопросы и ответы по StackOverflow дают хорошее представление о том, как управлять различными делами с помощью схемы JSON.

Также иногда может быть полезно проверить относительные JSON Pointers RFC.

...