Resource Status


#1

Recently I’ve faced controversial point.

I have a resource, let’s call it Product. And it’s got attribute state which could be: Draft or Active.
When it’s in Draft state - you are allowed to PATCH any attributes and relationships of the Resource. But once it enters Active state - you can PATCH only its prices relationship.

{
    "data": {
        "type": "Product",
        "id": "product-4",
        "attributes": {
            "state": "Draft",
            "name": "Super Product"
        },
        "relationships": {
            "prices": {
                "data": [
                    {
                        "type": "Price",
                        "id": "price-1"
                    }
                ]
            },
            "suppliers": {
                "data": [
                    {
                        "type": "Supplier",
                        "id": "supplier-2"
                    }
                ]
            }
        }
    }
}

What is the best way to describe such behavior? ALPS?


#2

Isn’t that specific to the implementation of the PATCH /products/id endpoint?

You just check state and the fields being updated and allow it or return an error.

Is that something that should be covered by JSON:API?


#3

It’s common for any API. What I want to achieve: let client to know what they can do with resource. Errors or ignoring change is reactive solution, which must be implemented for sure, but I’m talking about some kind of proactive solution. Interface which client application could rely to display only fields which are available to change.


#4

Could you give me an example of how an API can let the user know what they can do with a resource? It would help me understand your problem.


#5

We have the same thing in our API: A resource can exist in several different general states, and some states do not allow the resource to be modified, while others may allow some fields or operations.

What we did was this:

  • Attribute-specific constraints: The relevant resources have an attribute constraints. This is an object whose properties is a subset of the resoure’s attributes, but each of these constraint sub-properties has the same structure:

    {
      "relevant": true,
      "writable": true,
      "min": 1,
      "max": 5,
      ...
    }
    

    et cetera. (This would be the structure for a corresponding integer attribute. Other constraints may be relevant for other types.) We thus have a way of signalling to the client which attributes should be shown (where relevant is true), which attributes can currently be included in a PATCH (where writable is true), and further constraints on the value where relevant. The top-level constraints object can be excluded completely using sparse fieldsets (it’s a normal attribute, after all), and we have the rule that if a resource attribute is excluded using sparse fieldsets, the corresponding constraints sub-property is hidden, too.

  • When it comes to whether PATCH is allowed at all: We just have a property isReadonly on the resource that, when true, indicates that PATCH is not possible. (Strictly speaking we have a readonlyReason that when null allows PATCH, and when not null provides a machine-parsable reason why PATCH is not allowed. Note that other custom operations may be allowed which may then put the resource in a state that allows PATCH.)

  • We have a similar attribute called canDelete.

  • When it comes to other operations (custom links), they are simply allowed if and only if the links exist.

All of the above is specified clearly in our API specification. AFAIK JSON-API specifies no standards for these use-cases.


#6

Thanks for sharing. Seems interesting. It could be placed in meta.


#7

Yes, I considered that originally, but ultimately I decided that it was semantically just normal attributes (e.g. you could just as well have attributes count, countWritable, countMin, countMax, etc.). It made more sense to me to just have the constraints under attributes (but in a separate constraints container that could be excluded as a whole using sparse fieldsets, and to not clutter up the attributes list).

Plus, considering our stack, it would be higher friction to use meta extensively.


#8

So I had the same issue. In our case all state transitions are explicit, so main resources only support GET. What we then did was to have each resource emit a JSON Schema which lists all possible attributes plus all possible state transitions as links in the resource, with inline schema. Example:
{
“$schema”: “http://json-schema.org/draft-07/schema#”,
“type”: “object”,
“definitions”: {
“resource”: {
“type”: “object”,
“required”: [
“type”,
“id”
],
“additionalProperties”: false,
“properties”: {
“type”: {
“type”: “string”,
“const”: “thread”
},
“id”: {
“type”: “string”
},
“attributes”: {
“$ref”: “#/definitions/attributes”
}
}
},
“attributes”: {
“type”: “object”,
“additionalProperties”: false,
“required”: [
…stuff…
],
“properties”: {
…stuff…
}
}
},
“properties”: {
“data”: {
“$ref”: “#/definitions/resource”
}
},
“links”: [
{
“rel”: “likevideo”,
“href”: “{+command_href}”,
“templateRequired”: [
“command_href”
],
“submissionMediaType”: “application/vnd.api+json”,
“submissionSchema”: {
“type”: “object”,
“properties”: {
“data”: {
“type”: “object”,
“properties”: {
“attributes”: {
“type”: “object”,
“required”: [
],
“properties”: {
}
}
}
}
}
},
“targetPointers”: {
“command_href”: “/data/links/likevideo”
}
}, … more actions …
]
}

The trick is that the action href needs to be resolved against the actual JSON API document. So the schema shows all possible actions, including developer notes and such documentation (not included above), and is designed to be highly cacheable, but the actual actions available will be determined by the links section in the resource object. The href for the action will be to the same resource URL but with an additional ?profile= parameter. For POSTs (submissions) the client just post the data there, given that it knows what attributes should be in there based on the schema, and for PATCHes (updates) the client would do a GET first to see current state, update that, then PATCH it in.

With this approach we get explicit state transitions, client always knows exactly what is doable given current state, and what to submit, and we have a clear separation between the static schema (what is possible) and the individual current resource state (current attributes and links to available actions). At this point I don’t see us using the resource relationships feature in JSON API at all, and instead rely on explicit state transitions to model those. For example, if a resource had e.g. “…/relationships/comments” as a relationships resource, there are still tons of rules for whether a user can actually post a comment to that, which needs to be surfaced, and so we would just do that on “…/comments” resource instead. No implicit state transitions allowed basically.

For our purposes this is precisely what we need. The main trick was to figure out how to get JSON API and JSON Schema to each provide their different strengths between static and dynamic data.


#9

What does ALPS stand for?


#10

ALPS = Application-Level Profile Semantics
http://alps.io/spec/drafts/draft-01.html


#11

To the links.meta field of a relationship, I added an “allowed” array of HTTP verbs.

"relationships": {
    "prices": {
        "links": {
            "href": "https:/example.com/products/product-4/prices",
            "meta": {
                "allowed": ["GET", "PATCH"],
            }
        }
    }
}

#12

I would suggest using JSON Schema instead of ALPS. We started with ALPS but it gets awkward because it doesn’t have a clean way to express all the nuance of JSON, like types and enums. With JSON Schema all of that comes for free, basically.