Reputation: 2365
I am having trouble coming up with a JSON schema that will validate if the JSON contains either:
but not to match when multiples of those are present.
In my case specifically, I want one of
copyAll
fileNames
matchesFiles
and/or doesntMatchFiles
to validate but I don't want to accept when more than that is there.
Here's what I've got so far:
{
"$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"]}}]}
]
} ;
This matches more than I want to. I want this to match all of the following:
{"copyAll": true, "unrelatedA":"xxx"}
{"fileNames": ["aab", "cab"], "unrelatedA":"xxx"}
{"matchesFiles": ["a*"], "unrelatedA":"xxx"}
{"doesntMatchFiles": ["a*"], "unrelatedA":"xxx"}
{"matchesFiles": ["a*"], "doesntMatchFiles": ["*b"], "unrelatedA":"xxx"}
but not to match:
{"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"}
I'm guessing there's something obvious I'm missing - I'd like to know what it is.
Upvotes: 129
Views: 137525
Reputation: 71
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 valueyCode
and yDescription
must be present but only one needs to have a truthy valueimport { 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!
Upvotes: 1
Reputation: 71
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
Upvotes: 2
Reputation: 4975
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'
Upvotes: 3
Reputation: 13597
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"
]
}
]
}
]
}
Upvotes: 182