json-schema-org/json-schema-spec

How to interpret schema with both properties and oneOf?

bcherny opened this issue · 5 comments

I was wondering how to interpret the SqlConnectionInfo definition in the Azure JSON-Schema.

It looks like this:

  "SqlConnectionInfo": {
    "type": "object",
    "oneOf": [
      {
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "SqlConnectionInfo"
            ]
          }
        }
      }
    ],
    "properties": {
      "userName": {
        "type": "string",
        "description": "User name"
      },
      "password": {
        "type": "string",
        "description": "Password credential."
      },
      "type": {
        "type": "string"
      },
      ...
    },
    ...
  },

Full JSON-Schema: https://schema.management.azure.com/schemas/2017-11-15-privatepreview/Microsoft.DataMigration.json.

Is the right way to interpret this:

  1. SqlConnectionInfo is an object which may declare keys userName, password, and type
  2. If declared, the type field must be the string literal "SqlConnectionInfo"

Motivation: making sure that https://github.com/bcherny/json-schema-to-typescript/pull/603/files doesn't regress TypeScript typing generation for Azure's JSON-Schema.

Side note: it would be nice if the JSON-Schema Spec and docs more directly talked about how to handle complex schemas like this one, where more than one keyword applies (eg. if this schema definition moved the oneOf into type's definition, it would be much more clear how to interpret this schema). Or, let me know if I missed it in the docs.

@handrews any chance you could chime in? This is blocking bcherny/json-schema-to-typescript#603

@bcherny it's been a bit of a heavy month for some of us here. Sorry for missing this. (And BTW, Henry has shifted his focus more towards OpenAPI lately.)

This schema is written very strangely, IMO. Let's isolate the parts that are most likely confusing here.

{
  "oneOf": [
    {
      "properties": {
        "foo": {
          "type": "string",
          "enum": [ "SqlConnectionInfo" ]
        }
      }
    }
  ],
  "properties": {
    "foo": {
      "type": "string"
    }
  }
}

JSON Schema operates by declaring constraints on the instance (and locations within it). In this case, there are three constraints on the instance location /foo. That is, the value at that location within the data has three requirements:

  1. the type must be a string (declared by /oneOf/0/properties/foo/type)
  2. the value must be exactly "SqlConnectionInfo" (declared by /oneOf/0/properties/foo/enum)
  3. the type must be a string (declared by /properties/foo/type)

If you'll notice, (1) and (3) are the same requirement; it's just specified in two different places.
In practice, both (1) and (3) are also redundant because (2) requires an exact value.

My guess is that this schema was generated using a template that looks something like this:

{
  "oneOf": [
    <specific-requirements>
  ],
  "properties": {
    "foo": {
      "type": "string"
    }
  }
}

Then they replace <specific-requirements> with whatever they need. Just a theory though.


For your original schema, the SqlConnectionInfo property must be an object, and its type property MUST be the string SqlConnectionInfo.


We are currently working on updating the documentation. We have an open issue to add a recommendation about avoiding the redundancy.

I can speak to the code generation aspect:

@bcherny

Is the right way to interpret this:

  1. SqlConnectionInfo is an object which may declare keys userName, password, and type
  2. If declared, the type field must be the string literal "SqlConnectionInfo"

Both of these are correct. "oneOf" doesn't have any special interaction with "properties", both keywords must accept the input, and you can factor out, and distribute in, keywords over oneOf as you would logically expect. All three properties "userName, "password", and "type" will be optional—but when provided, must conform to the given criteria, and for "type" specifically, it must be exactly "SqlConnectionInfo".

Does that answer your question?

A few things to point out here: the aggregation keywords allOf/anyOf/oneOf are not "useful" unless there's two or more subschemas. Like @gregsdennis points out, such a schema is odd, as it's needlessly complicated. This has some considerations for code generation applications:

If it lists zero subschemas, it would be vacuously false; as such it describes the empty set, and would result in a type that cannot contain any values. I'm not aware of any way to notate such a thing. (anyOf is also vacuously false; but allOf is vacuously true.)

If it has one subschema, like your example, then allOf=anyOf=oneOf, and you can "factor out" all the keywords:

  "SqlConnectionInfo": {
    "type": "object",
    "properties": {
      "userName": {
        "type": "string",
        "description": "User name"
      },
      "password": {
        "type": "string",
        "description": "Password credential."
      },
      "type": {
        "type": "string",
        "enum": [
          "SqlConnectionInfo"
        ]
      }
      ...
    },
    ...
  },

(Also note how "type" becomes redundant in the presence of "enum" or "const".)

Finally, when you have two or more subschemas in "oneOf", then you can factor out keywords that all the subschemas have in common, as siblings to oneOf (except to the extent that one keyword affects/is read by another, in which case they must be refactored together; or if the keyword already exists with a different value). This applies for all three oneOf/anyOf/allOf because of how logical AND distributes.

Also note that, if I understand correctly, the | operator in TypeScript denotes a union, i.e. anyOf. You can only take anyOf=oneOf when the subschemas are completely disjoint from each other.

I will close this out since there's not a specific proposal for the specification, feel free to propose one or continue discussing here, though you may find better support in one of the venues dedicated to usage.

Thank you both for the great explanations -- that answers my question 🙏.