user3486184
user3486184

Reputation: 2365

How do I require one field or another or (one of two others) but not all of them?

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

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

Answers (4)

Hunter H.
Hunter H.

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 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!

Upvotes: 1

Miguel
Miguel

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

Steve
Steve

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

jruizaranguren
jruizaranguren

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

Related Questions