jsonschema

Can the if-keyword be used to modify a definition in JSON schema?


I've got a data structure I'm attempting to write a JSON Schema for, where many elements contain strings translated into one or more languages, here's a minimal example with English and French:

{
  "supportedLanguages": ["en", "fr"],
  "serviceName": {
    "en": "My App",
    "fr": "Mon Application"
  },
  "customerSupportEmail": {
    "en": "help@myapp.com"
  }
}

If I want to specify conditions, using JSON schema, so that "if English appears in supportedLanguages, every translated string (object with one key per language) must contain an English. I can do that per property, like this:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "example",
  "type": "object",
  "properties": {
    "supportedLanguages": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": ["en", "fr", "de"]
      },
      "minItems": 1,
      "uniqueItems": true
    },
    "serviceName": {
      "type": "object",
      "properties": {
        "en": {
          "type": "string"
        },
        "fr": {
          "type": "string"
        },
        "de": {
          "type": "string"
        }
      }
    },
    "customerSupportEmail": {
      "type": "object",
      "properties": {
        "en": {
          "type": "string"
        },
        "fr": {
          "type": "string"
        },
        "de": {
          "type": "string"
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "supportedLanguages": {
            "type": "array",
            "contains": {
              "const": "en"
            }
          }
        }
      },
      "then": {
        "properties": {
          "serviceName": {
            "type": "object",
            "required": ["en"]
          },
          "customerSupportEmail": {
            "type": "object",
            "required": ["en"]
          }
        }
      }
    }
   (... others, one per supported language, ommitted to make this snippet shorter)
  ]
}

This example works correctly, failing validation when a supportedLanguage value is present which is not the key for one of the fields.

Ideally I'd like to split the logic into a definition ($def), so it can be reused between many properties.

I tried doing it as follows:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "example",
  "type": "object",
  "properties": {
    "supportedLanguages": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": ["en", "fr", "de"]
      },
      "minItems": 1,
      "uniqueItems": true
    },
    "serviceName": {
      "$ref": "#/$defs/translatedString"
    },
    "customerSupportEmail": {
      "$ref": "#/$defs/translatedString"
    }
  },
  "$defs": {
    "translatedString": {
      "type": "object",
      "properties": {
        "en": {
          "type": "string"
        },
        "fr": {
          "type": "string"
        },
        "de": {
          "type": "string"
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "supportedLanguages": {
            "type": "array",
            "contains": {
              "const": "en"
            }
          }
        }
      },
      "then": {
        "$defs": {
          "translatedString": {
            "type": "object",
            "required": ["en"]
          }
        }
      }
    },
    (... skipped the other languages again)
  ]
}

but, testing with ajv in Node, this does not work.

Is there a way to add conditions to the definition inside the if-then-else structure?


Solution

  • You can modularize everything into $defs. here's an example of yours

    {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "$id": "example",
        "type": "object",
        "properties": {
            "supportedLanguages": {
                "type": "array",
                "items": {
                    "type": "string",
                    "enum": ["en","fr","de"]
                },
                "minItems": 1,
                "uniqueItems": true
            },
            "serviceName": {
                "$ref": "#/$defs/lang_kvType"
            },
            "customerSupportEmail": {
                "$ref": "#/$defs/lang_kvType"
            }
        },
        "allOf": [
            {
                "$ref": "#/$defs/english_lang"
            },
            {
                "$ref": "#/$defs/french_lang"
            },
            {
                "$ref": "#/$defs/german_lang"
            }
        ],
        "$defs": {
            "lang_kvType": {
                "title": "Language Key Value Pair",
                "propertyNames": {
                    "$ref": "#/$defs/CountryCodeType"
                },
                "additionalProperties": {
                    "type": "string"
                }
            },
            "CountryCodeType": {
                "title": "CountryCodeType",
                "description": "The 2 character country code. As per ISO 3166-1 Alpha 2",
                "type": "string",
                "maxLength": 2,
                "minLength": 2
            },
            "english_lang": {
                "if": {
                    "required": [
                        "supportedLanguages"
                    ],
                    "properties": {
                        "supportedLanguages": {
                            "type": "array",
                            "contains": {
                                "const": "en"
                            }
                        }
                    }
                },
                "then": {
                    "properties": {
                        "serviceName": {
                            "type": "object",
                            "required": [
                                "en"
                            ]
                        },
                        "customerSupportEmail": {
                            "type": "object",
                            "required": [
                                "en"
                            ]
                        }
                    }
                }
            },
            "french_lang": {
                "if": {
                    "required": [
                        "supportedLanguages"
                    ],
                    "properties": {
                        "supportedLanguages": {
                            "type": "array",
                            "contains": {
                                "const": "fr"
                            }
                        }
                    }
                },
                "then": {
                    "properties": {
                        "serviceName": {
                            "type": "object",
                            "required": [
                                "fr"
                            ]
                        },
                        "customerSupportEmail": {
                            "type": "object",
                            "required": [
                                "fr"
                            ]
                        }
                    }
                }
            },
            "german_lang": {
                "if": {
                    "required": [
                        "supportedLanguages"
                    ],
                    "properties": {
                        "supportedLanguages": {
                            "type": "array",
                            "contains": {
                                "const": "de"
                            }
                        }
                    }
                },
                "then": {
                    "properties": {
                        "serviceName": {
                            "type": "object",
                            "required": [
                                "de"
                            ]
                        },
                        "customerSupportEmail": {
                            "type": "object",
                            "required": [
                                "de"
                            ]
                        }
                    }
                }
            }
        }
    }
    

    EDIT, if you want to use enumerations for the country attribute names you can do this

    This will constrain the naming to only the enumerations provided, rather than a 2 character string value which I previously defined as CountryCodeType.

     "$defs": {
            "lang_kvType": {
                "title": "Language Key Value Pair",
                "propertyNames": {
                    "enum": ["ar", "ab", "ac", "en", "fr", "de"....]
                },
                "additionalProperties": {
                    "type": "string"
                }
            }