Skip to content

No method of reusing definitions in object without using a sub-object. #796

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

Closed
chaeron opened this issue Aug 28, 2019 · 11 comments
Closed

Comments

@chaeron
Copy link

chaeron commented Aug 28, 2019

The current Draft-8 spec (or any prior versions of JSON Schema) do not allow for a method of reusing a definition in an object directly, without embedding that definition as a sub-object.

For example, if I have timestamps defined as follows:

"$defs": {
"timestamps": {
            "type": "object",
            "properties": {
                "created_at": {
                    "type": "string",
                    "format": "date-time"
                },
                "updated_at": {
                    "type": "string",
                    "format": "date-time"
                },
            },
            "additionalProperties": false,
}

There is no way to directly "copy" the timestamp property definitions into a schema object where the created_at & updated_at properties are at the same level as the top level properties of the object. What I want is to be able to validate an object such as:

{
  "productId": 1,
  "productName": "A green door",
  "price": 12.50,
  "tags": [ "home", "green" ],
  "created_at":  "2019-07-28T12:27:45.507+00:00",
  "updated_at": "2019-08-28T12:27:45.507+00:00"
}

but using the timestamps definition (since timestamps could be used in many sub-objects in a schema).

Currently the only way to do this is to use a schema like the following:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://example.com/product.schema.json",
  "title": "Product",
  "description": "A product from Acme's catalog",
  "type": "object",
  "properties": {
    "productId": {
      "description": "The unique identifier for a product",
      "type": "integer"
    },
    "productName": {
      "description": "Name of the product",
      "type": "string"
    },
    "price": {
      "description": "The price of the product",
      "type": "number",
      "exclusiveMinimum": 0
    },
    "timestamps": { "$ref: "#/$defs/timestamps" }
  },
  "required": [ "productId", "productName", "price" ],
  "$defs": {
  "timestamps": {
            "type": "object",
            "properties": {
                "created_at": {
                    "type": "string",
                    "format": "date-time"
                },
                "updated_at": {
                    "type": "string",
                    "format": "date-time"
                },
            },
            "additionalProperties": false
  }
}

But that forces the created_at/updated to be in a sub-object as follows:

{
  "productId": 1,
  "productName": "A green door",
  "price": 12.50,
  "tags": [ "home", "green" ],
   "timestamps": {
    "created_at":  "2019-07-28T12:27:45.507+00:00",
    "updated_at": "2019-08-28T12:27:45.507+00:00"
   }
}

...which is not the desired result.

What I have done is to create a new applicator called $copy in my schemas (which I use for code generation in my particular application), so the schema looks like this:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://example.com/product.schema.json",
  "title": "Product",
  "description": "A product from Acme's catalog",
  "type": "object",
  "properties": {
    "productId": {
      "description": "The unique identifier for a product",
      "type": "integer"
    },
    "productName": {
      "description": "Name of the product",
      "type": "string"
    },
    "price": {
      "description": "The price of the product",
      "type": "number",
      "exclusiveMinimum": 0
    },
    "$copy": { "$ref: "#/$defs/timestamps" }
  },
  "required": [ "productId", "productName", "price" ],
  "$defs": {
  "timestamps": {
            "type": "object",
            "properties": {
                "created_at": {
                    "type": "string",
                    "format": "date-time"
                },
                "updated_at": {
                    "type": "string",
                    "format": "date-time"
                },
            },
            "additionalProperties": false
  }
}

but alas, that applicator is not part of the spec. Maybe it should be, to handle this particular use case?

Thanks for following along and giving the idea of a $copy applicator some thought.

.....Andrzej

@Relequestual
Copy link
Member

There is a solution to this already. allOf.
Additionally, your solution prevents the use of a property called $copy.

This boils down to where you can and cannot have a subschema.
You can only reference when your object is a schema.
The value of the properties keyword is not a schema, and so you cannot use schema keywords there.

It's fine to use allOf at the root level...

{
  "$defs": {...},
  "allOf": [
    {
      "$ref": "#/$defs/timestamps"
    },
    {
      "$ref": "#/$defs/otherThing"
    },
    {
      "properties": {
        "productId": {
          "description": "The unique identifier for a product",
          "type": "integer"
        }
      }
    }
  ]
}

You would have to remove the "additionalProperties": false from the definition though.
Draft-8 has a new keyword unevaluatedProperties to work round this issue.

@johandorland
Copy link
Collaborator

In my opinion this does not solve a problem that cannot be solved differently already, so 👎

@chaeron
Copy link
Author

chaeron commented Aug 28, 2019

Using allOf is a really awkward way of doing something that should be simple. ;-)

Yes....this prevents you from using $copy as a property, but only if you use a $ref with it. If a $copy property is defined using any schema other than $ref, you could still create such a property.

@Relequestual
Copy link
Member

I'm afraid your solution is far more akward when you look at the bigger picture.
Preventing people from using $ref if their value is $copy is worse. Special rules like that are uncool, for both schema authors and implementers.

@johandorland
Copy link
Collaborator

If you add obscure syntax for every little corner case you end up with a monstrosity of a specification.

@handrews
Copy link
Contributor

@chaeron $ref used to be possible anywhere and it caused a lot of problems.

In any event, people have been using allOf for this for years and the only problem is additionalProperties, which we have fixed by adding unevaluatedProperties which has the desired behavior when used with allOf.

Your solution complicates the processing model (it violates the principle that each schema object can be evaluated on its own by splicing two partial schemas together) and saves maybe one level of indentation somewhere. It is similar to $merge and we spent 500+ comments debating that and deciding not to use it, because when you really go deep on that approach it becomes very messy.

Note that we recommend against using $ as that is the prefix for the standard Core Vocabulary. We don't even use the $ in our own other vocabularies, it is only for Core. This is not a strict requirement, but it is a strong recommendation.

@chaeron
Copy link
Author

chaeron commented Aug 28, 2019

Thanks for all the considered responses.

When I get some time, I'll make my schema (and code generation that depends on it) to conform to the allOf approach for doing this kind of reuse of definitions.

@Relequestual
Copy link
Member

@chaeron You're welcome! Please do continue to ask questions and make suggestions. We strive to try and fully understand your point of view and requirements. It often turns out things are a lot more complex than we first anticipate, so you're in good company.

@chaeron
Copy link
Author

chaeron commented Aug 30, 2019

Where can I find more info about unevaluatedProperties, more specifically examples of usage?

Thanks!

@handrews
Copy link
Contributor

@chaeron there will be more as we publish the spec. Right now the same people working to finish the spec are the ones who will need to add guides and examples on the web site. And everyone's on vacation b/c end of summer / labor day weekend. Check back in a few weeks!

@chaeron
Copy link
Author

chaeron commented Aug 30, 2019

Thanks....I posted my question too soon. Found a good explanation here:

#556

Posting in case anyone else is looking, prior to finalization of the spec and revision of examples/documentation. ;-)

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

No branches or pull requests

4 participants