如何需要一个字段或另一个字段或(两个其他字段之一) ,但不需要所有字段?

如果 JSON 包含以下内容,我就很难找到一个 JSON 模式来验证:

  • 只有一块地
  • 只有另一个领域
  • (其他两个字段中的一个)

但是当有很多这样的情况出现的时候就不一样了。

就我而言,我想要一个

  • copyAll
  • fileNames
  • matchesFiles及/或 doesntMatchFiles

但我不想接受,当有更多的时候。

以下是我目前为止得到的信息:

{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [ "unrelatedA" ],
"properties": {
"unrelatedA": {
"type": "string"
},
"fileNames": {
"type": "array"
},
"copyAll": {
"type": "boolean"
},
"matchesFiles": {
"type": "array"
},
"doesntMatchFiles": {
"type": "array"
}
},
"oneOf": [
{"required": ["copyAll"], "not":{"required":["matchesFiles"]}, "not":{"required":["doesntMatchFiles"]}, "not":{"required":["fileNames"]}},
{"required": ["fileNames"], "not":{"required":["matchesFiles"]}, "not":{"required":["doesntMatchFiles"]}, "not":{"required":["copyAll"]}},
{"anyOf": [
{"required": ["matchesFiles"], "not":{"required":["copyAll"]}, "not":{"required":["fileNames"]}},
{"required": ["doesntMatchFiles"], "not":{"required":["copyAll"]}, "not":{"required":["fileNames"]}}]}
]
} ;

这比我想要的更匹配。我希望它匹配以下所有内容:

{"copyAll": true, "unrelatedA":"xxx"}
{"fileNames": ["aab", "cab"], "unrelatedA":"xxx"}
{"matchesFiles": ["a*"], "unrelatedA":"xxx"}
{"doesntMatchFiles": ["a*"], "unrelatedA":"xxx"}
{"matchesFiles": ["a*"], "doesntMatchFiles": ["*b"], "unrelatedA":"xxx"}

但不匹配:

{"copyAll": true, "matchesFiles":["a*"], "unrelatedA":"xxx"}
{"fileNames": ["a"], "matchesFiles":["a*"], "unrelatedA":"xxx"}
{"copyAll": true, "doesntMatchFiles": ["*b"], "matchesFiles":["a*"], "unrelatedA":"xxx"}
{"fileNames": ["a"], "matchesFiles":["a*"], "unrelatedA":"xxx"}
{"unrelatedA":"xxx"}

我想我肯定漏掉了什么,我想知道是什么。

90770 次浏览

The problem is the "not" semantics. "not required" does not mean "inclusion forbidden". It just means that you don't have to add it in order to validate that schema.

However, you can use "oneOf" to satisfy your specification in a simpler way. Remember that it means that "just one of these schemas can validate". The following schema achieves the property switching you are attempting to solve:

{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [
"unrelatedA"
],
"properties": {
"unrelatedA": {
"type": "string"
},
"fileNames": {
"type": "array"
},
"copyAll": {
"type": "boolean"
},
"matchesFiles": {
"type": "array"
},
"doesntMatchFiles": {
"type": "array"
}
},
"oneOf": [
{
"required": [
"copyAll"
]
},
{
"required": [
"fileNames"
]
},
{
"anyOf": [
{
"required": [
"matchesFiles"
]
},
{
"required": [
"doesntMatchFiles"
]
}
]
}
]
}

This answer is not related to JSON schema, so it's a bit off the track, though it can bring another perspective on solving this problem, and json validation in general.

The point is to express declaratively exactly what you need as a result: a single field which is the only present. Consider the following json schema:

JsonElement json =
new Gson().toJsonTree(
Map.of(
"first_field", "vasya",
"second_field", false,
"third_field", 777,
"unrelated", "Rinse"
)
);

Let's say you need either one of the first_field, second_field, and third_field. The fourth field doesn't matter. Here is how the corresponding validation object looks like:

Result<SomeTestStructure> result =
new UnnamedBlocOfNameds<SomeTestStructure>(
List.of(
new OneOf(
"global",
new ErrorStub("Only one of the fields must be present"),
new AsString(
new Required(
new IndexedValue("first_field", json)
)
),
new AsBoolean(
new Required(
new IndexedValue("second_field", json)
)
),
new AsInteger(
new Required(
new IndexedValue("third_field", json)
)
)
),
new AsString(
new IndexedValue("unrelated", json)
)
),
SomeTestStructure.class
)
.result();

First, you declare an unnamed block consisting of named ones; then you say that you need a single successful validatable element out of the three ones. And finally, you declare what success means. In this case, to be successful is to be simply present. If json is valid, an object of SomeTestStructure class is created:

assertTrue(result.isSuccessful());
assertEquals(
new SomeTestStructure(777, "Rinse").thirdField(),
result.value().raw().thirdField()
);

For more info about this approach and a library implementing it, check out a quick start entry.

If the property having a value of null is as good as it not being there, then something like this might be suitable. commonProp must be provided, and only one of x or y can be provided.

You might get a couple of similar error messages though.

{
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
required: ['commonProp'],


oneOf: [
{
properties: {
x: { type: 'number' },
commonProp: { type: 'number' },
y: {
type: 'null',
errorMessage: "should ONLY include either ('x') or ('y') keys. Not a mix.",
},
},
additionalProperties: { not: true, errorMessage: 'remove additional property ${0#}' },
},
{
properties: {
y: { type: 'number' },
commonProp: { type: 'number' },
x: {
type: 'null',
errorMessage: "should ONLY include either ('x') or ('y') keys. Not a mix.",
},
},
additionalProperties: { not: true, errorMessage: 'remove additional property ${0#}' },
},
],
}
const model = { x: 0, y: 0, commonProp: 0 };


// ⛔️ ⛔️ ⛔️ ⛔️ ⛔️ ⛔️
// Model>y should ONLY include either ('x') or ('y') keys. Not a mix.
// Model>x should ONLY include either ('x') or ('y') keys. Not a mix.
const model = { x: 0, y: null, commonProp: 0 };


// ✅ ✅ ✅ ✅ ✅ ✅
const model = { x: 0 };


// ⛔️ ⛔️ ⛔️ ⛔️ ⛔️ ⛔️
// Model must have required property 'commonProp'

As pointed out by @Tomeamis in the comments, the not-required combination means "forbidden" in json schema. However, you should not duplicate the "not" keyword (I do not really know why). Instead you should

{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [ "unrelatedA" ],
"properties": {
"unrelatedA": {
"type": "string"
},
"fileNames": {
"type": "array"
},
"copyAll": {
"type": "boolean"
},
"matchesFiles": {
"type": "array"
},
"doesntMatchFiles": {
"type": "array"
}
},
"oneOf": [
{
"required": [
"copyAll"
],
"not": {
"anyOf": [
{"required":["matchesFiles"]},
{"required":["doesntMatchFiles"]},
{"required":["fileNames"]}
]
}
},
{
"required": [
"fileNames"
],
"not": {
"anyOf": [
{"required":["matchesFiles"]},
{"required":["doesntMatchFiles"]},
{"required":["copyAll"]}
]
}
},
{
"anyOf": [
{
"required": ["matchesFiles"],
"not": {
"anyOf": [
{"required":["fileNames"]},
{"required":["copyAll"]}
]
}
},
{
"required": ["doesntMatchFiles"],
"not": {
"anyOf": [
{"required":["fileNames"]},
{"required":["copyAll"]}
]
}
}]
}
]
}

More details here

To forbid the presence of a property it is also possible to do

{
"properties": {
"x": false
}
}

as mentioned in the answers here

Little late to the party here but I implemented a solution today for this that works in my schema and is reusable.

For context, I had several fields that were required by name but their value could be empty or required to be present based on another condition.


Here is the reusable TypeScript method:

// SchemaLogic.ts


import { Schema } from "jsonschema";


/**
* A required string property with a minimum length of 0.
*/
export const StringValue = { type: "string", required: true, minLength: 0 };
/**
* A required string property with a minimum length of 1.
*/
export const NonEmptyStringValue = { type: "string", required: true, minLength: 1 };


/**
* Provides the option to submit a value for one of the two
* property names provided. If one of the properties is
* submitted with a truthy string value, then the other will
* not be required to have a value. If neither are submitted
* with a truthy value, then both will return an error
* message saying that the minimum length requirement has
* not been met.
*
* **NOTE:**
*  1. this only works with string properties that are
*     not restricted to a certain set of values or a
*     regex-validated format
*  1. this must be used inside an `allOf` array
*
* @param propertyNames the names of the properties
* @returns a {@link Schema} that creates a conditional
*  requirement condition between the two fields
*/
export const eitherOr = (propertyNames: [string, string]): Schema => {
return {
if: { properties: { [propertyNames[0]]: NonEmptyStringValue } },
then: { properties: { [propertyNames[1]]: StringValue } },
else: {
if: { properties: { [propertyNames[1]]: NonEmptyStringValue } },
then: { properties: { [propertyNames[0]]: StringValue } },
else: {
properties: {
[propertyNames[0]]: NonEmptyStringValue,
[propertyNames[1]]: NonEmptyStringValue,
},
},
},
};
};

And here is the most basic example of how to use it. This will require the following:

  • xCode and xDescription must be present but only one needs to have a truthy value
  • yCode and yDescription must be present but only one needs to have a truthy value
import { eitherOr } from "./SchemaLogic";


const schema: Schema = {
allOf: [eitherOr(["xCode", "xDescription"]), eitherOr(["yCode", "yDescription"])],
};

If you want to get more complex and require these fields conditionally, you can use something like the following:

const schema: Schema = {
properties: {
type: {
type: ["string"],
enum: ["one", "two", "three"],
required: true,
},
},
if: {
// if the 'type' property is either "one" or "two"...
properties: { type: { oneOf: [{ const: "one" }, { const: "two" }] } },
},
then: {
// ...require values
allOf: [eitherOr(["xCode", "xDescription"]), eitherOr(["yCode", "yDescription"])],
},
};

Note:

If your schema uses additionalProperties: false, you will need to add the properties to the 'properties' section of your schema so they are defined. Otherwise, you will have a requirement for the field to be present and, at the same time, not allowed because it's an additional field.

Hope this is helpful!