Dynamic constraints - a proposal for a general specification

In a few of my APIs, I have had the need to indicate to the API client (front-end) dynamic constraints for certain attributes and relationships. In working with this, I have produced a specification that works well for us.

I post it here with two goals:

  • In the hopes that it is useful as-is to the JSON:API community
  • To get feedback: Any obvious weaknesses? Significant improvements? Could this work as a JSON:API v1.1 profile?

Specification for dynamic constraints

This specification is intended to facilitate consistent communication of dynamic field restrictions, i.e. restrictions that may differ during the lifetime of a resource or between different resources of the same type.

Resources MAY have a constraints attribute. The constraints attribute MUST be considered a read-only attribute. The constraints attribute MUST be an object (called a resource constraints collection) whose property names match resources field names. The value of each such property MUST be an object (called field constraints collection). Each field constraints collection property (called constraint) describes a particular constraint for the corresponding resource field.

Different APIs will differ in which constraints are needed. This specification is agnostic regarding the actual constraints and their semantic meaning. This should be defined for each API/resource.

Below is an example showing a resource with three potential constraints; requiredForPublish, whitelist and writable.

{
  "data": {
    "type": "articles",
    "id": "92a34212",
    "attributes": {
      "category": "tech",
      "title": "Try JSON:API!",
      "isPublished": false,
      "constrains": {
        "category": {
          "requiredForPublish": true,
          "whitelist": [ "tech", "music"]
        },
        "isPublished": {
          "writable": true
        },
        "author": {
          "writable": false
        }
      },
      "relationships": {
        "author": {
          "data": {
            "type": "person",
            "id": "aad385f1"
          }
        }
      }
    }
  }
}

In the example above:

  • The category attribute is “required for publishing” (the exact semantics are defined by the API, but we can assume that it must be non-null before we can set isPublished to true), and constrained to be either tech or music (it may e.g. be an enum with additional documented values, but the set of allowed values may depend on the privileges of the current user).
  • The title attribute is not constrained.
  • The isPublished attribute is currently writable (based on the above, we can assume writable would be false if category is null).
  • The author relationship is not writable. (For example, the user may not have privileges to change the article’s author.)

To clarify the nomenclature:

  • The value of the constraints attribute is called the resource constraints collection.
  • Each constraints property (category, isPublished, author) is called a field constraints collection.
  • Each field constraints collection property (requiredForPublish, whitelist, writable) is called a constraint.

Constraints MAY be effectively no-op, e.g. "minLength": 0 or "writable": true. Front-end developers may want to take this into account so the user won’t see a message saying “at least 0 characters” below an input field.

Constraints MAY be excluded from a response if they are effectively no-op. This may be done at any level: for individual constraints, for field constraint collections (if it only has no-op constraints), or for the top-level resource constraints collection (excluding the constraints attribute if all constraints are no-op).

When resource fields are excluded using sparse fieldsets, the corresponding field constraints collection MAY also be excluded. (In the example above, if specifying fields[articles]=category,constraints, then constraints.isPublished and constraints.author may be excluded.)

Constraints are intended for dynamic restrictions, i.e. restrictions that may differ during the lifetime of a resource or between different resources of the same type. Statically known constraints, such as if a field is permanently read-only or restricted to known values (i.e. enums) or a specific length (e.g. due to database constraints), may be part of the API specification instead of being modeled using constraints. There are however valid use-cases for using constraints for what is effectively static restrictions (or lack of restrictions). For example:

  • A writable constraint on a field that is currently permanently writable can ensure backwards compatibility if it must later be made read-only (permanently or dynamically).
  • A writable constraint on a field that is currenly permanently read-only can ensure API clients automatically support writing to the field if it is made writable.

In such cases, however, the restrictions should only be supplied using constraints, not statically in the API specification, since any statically specified constraints indicate that they may be hardcoded into API clients.

Furthermore, returning a writable (or similar) constraint for fields that are currently permanently read-only must be weighed up against the complexity for API clients to support these fields. If it is not likely that a field will ever be made writable, it may be better to indicate this statically in the API specification, since this may enable front-ends to use more suitable controls (such as displaying text directly vs. using a disabled input.)

Apart from the requirements in this specification, the constraints attribute is a normal resource attribute as per the JSON:API specification.

1 Like

This sounds like a great candidate for a profile.

I would recommend using a profile instead of an extension for two reasons:

  1. It does not need the power of extension. The proposal can be standardized defining implementation semantics only. I would recommend avoiding to use an attribute. Instead resource-object’s meta seems to be a better fit. And it is less likely to naming conflicts with implementations or other profiles.
  2. Constraints provide additional information for a resource object. It valuable for a client but not required to understand the response. This also is a strong argument for profiles. A client can ignore a applied profile it does not support. But a client must reject a request if it does not support an applied extension.

I would recommend standardizing some constraints. This enables reusable libraries. As in the base specification, you could specify profile-defined constraints while still allowing implementation-specific constraints. The base specification typically reserves member names starting with lower case character to the specification itself and member names starting with a uppercase character to implementations. I would recommend following that pattern.

You need to define an URI identifying the profile. I recommended using a GitHub project for it. This provides documentation and collaboration practices developers are used to.

Thanks for your feedback! I agree with profile and not extension.

Regarding your point 1: I do see your point with meta (which I originally considered); however, I disagree with it for two reasons. One is objective: By using an attribute, it is possible for clients to use sparse fieldsets to exclude all constraints, which is not possible if using meta and which many read-only clients may want to do to reduce the response size. (This is the reason I went with an attribute.) Another is subjective: We have been making extensive use of the constraints attribute as defined here in many APIs for the last several years. It will be hard to change this now, and I am not really willing to create and implement a profile that we do not adhere to.

The base specification typically reserves member names starting with lower case character to the specification itself and member names starting with a uppercase character to implementations.

Unless I’m mistaken, the base spec doesn’t care about the first character, but all characters. So a name like foobar is reserved, but fooBar is not. (This is the case for query parameters, at least.)

You are right! Thanks a lot for clarifying.

That’s a valid argument. But I feel it would be better served by having a query parameter to define for which fields the constraints should be sent. Basically a companion of fields but for the constraints.

Currently the base specification forbids profiles to define query parameters. But I think that’s wrong. Implementation can define query parameters. Profiles can specify any implementation semantics. Unless I’m missing something I don’t see any reasons why profiles shouldn’t be allowed to define query parameters. As long as they follow the same naming rules as implementation-specific query parameters.

I think long term a fieldsConstraints query parameter allowing a client to request inclusion of constraints for specific fields, would be best. Maybe supporting a special character (e.g. *) to allow inclusion of constraints for all included fields.

I understand that point. On the other hand it’s less likely that others adopt your profile if it has limitations such as this. A valuable compromise could be defining both as two different versions and not switching your implementations. If others adopt your profile and invest in the libraries you are using, you could switch with less costs to the better approach. But you are not forced to invest know.

I created Profiles should be allowed to define query parameters · Issue #1718 · json-api/json-api · GitHub to discuss and address that problem.