Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to interpret schema with both properties and oneOf? #1529

Closed
bcherny opened this issue Jun 25, 2024 · 5 comments
Closed

How to interpret schema with both properties and oneOf? #1529

bcherny opened this issue Jun 25, 2024 · 5 comments
Labels

Comments

@bcherny
Copy link

bcherny commented Jun 25, 2024

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.

@bcherny
Copy link
Author

bcherny commented Jul 21, 2024

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

@bcherny bcherny changed the title How to interpret this schema? How to interpret schema with both properties and oneOf? Jul 21, 2024
@gregsdennis
Copy link
Member

@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.

@awwright
Copy link
Member

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.

@bcherny
Copy link
Author

bcherny commented Jul 22, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants