json-schema-org / json-schema-spec

The JSON Schema specification
http://json-schema.org/
Other
3.82k stars 266 forks source link

Question: $defs name meaning of convert generic types to json schema #1415

Closed xiaoxiangmoe closed 1 year ago

xiaoxiangmoe commented 1 year ago

In typescript we have code

export type List<Item> = Item[]
export type StrList = List<string>

The blog of @gregsdennis Using Dynamic References to Support Generic Types told me that I can use schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "definitions": {
    "List": {
      "$defs": {
        "foobar1": {
          "$dynamicAnchor": "Item",
          "not": true
        }
      },
      "type": "array",
      "items": {
        "$dynamicRef": "#Item"
      }
    },
    "StrList": {
      "description": "str array",
      "$ref": "#/definitions/List",
      "$defs": {
        "foobar2": {
          "$dynamicAnchor": "Item",
          "type": "string"
        }
      }
    }
  },
  "$ref": "#/definitions/StrList"
}

to validate value ["1","2"].

And I can see @jdesrosiers 's Hyperjump - JSON Schema Validator already support it.


Question:

gregsdennis commented 1 year ago

The way that you've defined your lists isn't really taking advantage of the pattern. (Also I think it should be invalid because you've defined the same $dynamicAnchor twice in the same schema resource. cc: @jdesrosiers)

You want to separate the List<Item> schema from the List<string> schema.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "generic-list",
  "$defs": {
    "foobar1": {
      "not": true,
      "$dynamicAnchor": "item"
    }
  },
  "type": "array",
  "items": {
    "$dynamicRef": "#Item"
  }
}

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "list-of-string",
  "$defs": {
    "foobar2": {
      "$dynamicAnchor": "item",
      "type": "string"
    }
  },
  "$ref": "generic-list"
}

They don't necessarily be in separate documents, but they do each need their own $ids. (Also, your top-level schema has definitions where it should use $defs.


In regard to your questions, foobar1 and foobar2 are just item definitions.

foobar1 is required because the 2020-12 spec says that $dynamicRef only works dynamically if the schema resource also contains a $dynamicAnchor with the same name. We call this the "bookending" requirement, and we're removing it for the next version of the spec.

foobar2 effectively "overrides" foobar1 when you start evaluation from the list-of-string schema. $dynamicRef takes the first $dynamicAnchor in the "dynamic scope." Each schema resource you enter during evaluation adds to the dynamic scope. So when you start with the list-of-strings schema, that schema is added to the dynamic scope. Then the $ref takes you to the generic-list schema, and that's added to the dynamic scope. The first $dynamicAnchor in that dynamic scope is the one that $dynamicRef uses.

The names you choose don't matter to JSON Schema. Choose something that makes sense to you. For this example, I'd use generic-item for foobar1 and string-item for foobar2. Ultimately that key in $defs is ignored since the $dynamicRef is looking at the $dynamicAnchor instead of the relative path to the definition.

xiaoxiangmoe commented 1 year ago

they do each need their own $ids

I think we need't have their own $ids and we should allow defined the same $dynamicAnchor twice in the same schema resource.

Here is the use case in OpenAPI v 3.1

{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "servers": [
    {
      "url": "http:hello.example.com",
      "description": "Generated server url"
    }
  ],
  "tags": [
    {
      "name": "HelloController",
      "description": "hello"
    }
  ],
  "paths": {
    "/hello/world": {
      "get": {
        "tags": [
          "HelloController"
        ],
        "summary": "hello",
        "operationId": "helloControllerGetWorld",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/List",
                  "$defs": {
                    "foobar2": {
                      "$dynamicAnchor": "Item",
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "List": {
        "$defs": {
          "foobar1": {
            "$dynamicAnchor": "Item",
            "not": true
          }
        },
        "type": "array",
        "items": {
          "$dynamicRef": "#Item"
        }
      },
     "Array": {
        "$defs": {
          "foobar1": {
            "$dynamicAnchor": "Item",
            "not": true
          }
        },
        "type": "array",
        "items": {
          "$dynamicRef": "#Item"
        }
      }
    }
  }
}
xiaoxiangmoe commented 1 year ago

Here is also a question:

How can I translate bounded generics types into json schema?

export type Role = 'admin' | 'user';
export type List<Item extends Role> = Item[];
gregsdennis commented 1 year ago

I think we need't have their own $ids and we should allow defined the same $dynamicAnchor twice in the same schema resource.

It doesn't make sense to define multiple in a single resource. They're effectively identifiers. If you have more than one, you have an ambiguous resolution, and it will fail.

It's not explicitly stated in the spec, but this uniqueness is implied by the resolution behavior.

gregsdennis commented 1 year ago

How can I translate bounded generics types into json schema?

This sort of conversion isn't defined. Honestly, none of this is defined; it's just a pattern I came up with for use by anyone who finds it useful.

xiaoxiangmoe commented 1 year ago

Should we open an issue to discuss support for bounded generics types? This makes sense for statically typed language translations.

gregsdennis commented 1 year ago

Not in this repo. There's a vocab-idl repo that is better suited for this kind of topic.

jdesrosiers commented 1 year ago

It doesn't make sense to define multiple in a single resource. They're effectively identifiers. If you have more than one, you have an ambiguous resolution, and it will fail.

I don't think it's technically correct for a validator to fail in this case, but it is ambiguous. You might end up at one of the dynamic anchors or the other. It's just like having duplicate property names in JSON. It's not in valid JSON, but when you try to access that property, some implementations might give the first and another might give you the last.

And I can see @jdesrosiers 's Hyperjump - JSON Schema Validator already support it.

Your schema only accidentally works and it's because of this ambiguity. My implementation ends up resolving to the last dynamic anchor (with the same value) that it encounters when the schema is evaluated. If you switch the order in which your definitions are declared, you would get a different result because the dynamic anchors would be evaluated in a different order.

Here is the use case in OpenAPI v 3.1

As Greg said, $ids are necessary. Within a single schema resource, a dynamic reference has no behavior that's different than a normal reference. There's no reason to use a dynamic reference unless you're referencing a different schema resource. That said, all you have to do to make your OpenAPI example work is to give your schemas $ids. When a schema has an $id, it becomes a distinct schema resource.