Description
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 anapplication/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.
- Non-keyword property names are allowed in order to facilitate JSON Schema extensions that can opt to apply
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.