Skip to content

Overriding annotation/usage keywords at point of use #98

Closed
@handrews

Description

@handrews

Use Case and Motivation

This is one of several proposals to come out of the analysis of the "ban additional properties mode" vs $merge/$patch debate and the various use cases that fed into that.

One common use case identified was specifying information about a type's usage at the point of use rather than as part of the initial type definition. In some cases, it is reasonable to provide some generic usage information in the type definition which serves as default usage documentation, but needs to be overridden in some uses with more specific information.

Additionally, default cannot meaningfully appear in multiple branches of logical keywords such as allOf, as there is no sensible way to choose which value is correct for the specific usage. Conflicting validations in an allOf simply fail the allOf, but conflicting default values are just unusable, independent of validation.

Prior experience

This particular use case has been a major pain point on both large-scale JSON Schema projects with which I have been involved.

One implemented $merge, which was very effective but (as several people have noted, and as I now agree) it is too powerful as it (and $patch) can literally convert any schema into an arbitrarily different schema, or even into a non-schema JSON value. In this project, $merge was used to override default values for default, title, description, and readOnly, or disambiguate among several possible values for those keywords.

The other project ended up redefining many types that should be sharing a definition. In some cases, nearly every schema has had to redefine a particular sub-schema just to change the title and description.

Preserving independent validation

In order to preserve the self-contained nature of validation, usage overriding only applies to the keywords in the "Metadata keywords" section of the validation specification (title, description, and default) plus usage-oriented hyper-schema keywords (readOnly and pathStart).

Breaking self-contained validation has been the main objection to other proposals in this area, either through overly powerful keywords or changing validation modes in ways which are not reflected in the schema itself. This is the only proposal to date that solves this problem with no impact on validation whatsoever.

Existing workarounds

It is possible to use an allOf with a reference to the type schema and a schema that includes only the point-of-use annotations. This is tolerable when reading the schema, but is not good for documentation generation. Each schema within an allOf should be self-contained, which separates the documentation from the thing it's documenting. Additionally, if default annotation fields have been filled out, a documentation tool that attempts to extract and combine annotation data from all allOf branches will just produce conflicting documentation.

Finally, allOf is nonsensical when combining multiple schemas using default, readOnly, or pathStart, as there is no reasonable way to choose which value to use.

The proposal: $use

The keyword $use would be an annotation/hyperschema keyword indicating that a type (via a $ref) is being used with additional annotation and/or hypermedia for this specific use.

$use is an object with two required properties and one optional property; the format for the required properties is borrowed from the $merge and $patch proposals:

  • source MUST be a schema, and will nearly always be a $ref as there is no need for $use if you have the literal schema defined in line. Required.
  • with should be applied to the resolved source as an application/merge-patch+json instance, which MUST NOT affect any JSON Schema-defined keyword that is not explicitly allowed by the proposal. Required.
    • Non-keyword property names are allowed in order to facilitate JSON Schema extensions that can opt to apply $use to their extensions or not as they choose. Required.

JSON Merge Patch considerations

Declaring the with object to follow JSON Merge Patch semantics allows implementations to use existing libraries, and frees us up from needing to debate yet another schema combination format.

However, a notable limitation of JSON Merge Patch is that you cannot overwrite something to null, as specifying a null value instead deletes the key entirely. A workaround for this that is consistent with existing JSON Schema behavior is to define null somewhere (perhaps right in the $use object since it does not forbid additional properties) and then $ref it. We would need to declare that $use is processed before $ref. Which is fine because $ref often needs lazy evaluation due to circular references anyway.

Elsewhere, there's been an assertion that $ref must always resolve to a schema. This would require breaking that assumption. Since the assumption was not present in Draft 04 (in which JSON Reference was an entirely separate specification), I think this isn't too unreasonable. A compromise could be that it can reference either a schema or null.

If $ref-ing null doesn't gain acceptance, another option would be an additional keyword in the $use object listing JSON pointers to properties that should be set to null.

"$" in name

I chose $use rather than use on the grounds that keywords that manipulate the schema itself should stand out (I will be filing a more comprehensive issue on the use of $ later as it comes up in yet another not-yet-filed proposal, plus there is an open issue to consider $id in place of id).

Example

{
    "type": "object"
    "properties:" {
        "interestTimestamp": {
            "$use": {
                "source": {"$ref": "#/definitions/specialTimestamp"},
                "with": {
                    "title": "Last Event of Interest",
                    "description": "The last time that something interesting happened.  Cannot be directly updated through the API.",
                    "readOnly": true,
                }
            }
        }
    },
    "definitions": {
        "specialTimestamp": {}
    }
}

This shows that the type can be concisely defined in a shared location, with a clear usage object applied to it.

Meta-schema considerations

This could be added to the meta-schema by moving most of the keyword definitions into the meta-schema's definitions section grouped by type of keyword, and defining with as a schema minus the forbidden keywords.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions