Skip to content
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

Supporting schemas for Action request bodies #84

Open
kevinswiber opened this issue May 28, 2017 · 25 comments
Open

Supporting schemas for Action request bodies #84

kevinswiber opened this issue May 28, 2017 · 25 comments

Comments

@kevinswiber
Copy link
Owner

See some discussion on #65.

Hi, folks. I know a solution to this topic is long-awaited.

Copying an example from the above thread (specifically, @xogeny's), here's a proposal for actions that have a request body with a typed schema of some sort.

I think I'd like to see Siren be explicit about whether the schema is inline or external.

Inline:

"actions": [
  {
    "name": "addNewProduct",
    "title": "Add a new product (lots of structured data required)",
    "method": "POST",
    "href": "http://api.x.io/products",
    "type": "application/json",
    "schema": {
       "value": {
         "$schema": "http://json-schema.org/draft-04/schema#",
         "title": "Product",
         "description": "A product from Acme's catalog",
         "type": "object"
        }
      }
    }
  ]

External:

"actions": [
  {
    "name": "addNewProduct",
    "title": "Add a new product (lots of structured data required)",
    "method": "POST",
    "href": "http://api.x.io/products",
    "type": "application/xml",
    "schema": {
       "src": "http://api.x.io/schemas/productSchema.xsd"
      }
    }
  ]

Of course, we need language stating that if a schema exists, fields should not be present. And probably more, but we can work that out. Happy to listen to any feedback.

@xogeny
Copy link
Contributor

xogeny commented May 28, 2017

Of course, I'm all for it. But I'm confused on one point. You talk about "inline" vs. "external". But what isn't clear to me is whether you are talking about "Inline JSON Schema" vs. "External JSON Schema" or whether you are talking about "Inline JSON Schema" vs. "External non-JSON Schema". At first, I thought it was the former (and I had actually prepared a response on that point), but in looking at your example now I suspect you actually meant the latter. Could you clarify?

@kevinswiber
Copy link
Owner Author

@xogeny

Sure.

Inline means the schema is in the message. This could be an XSD as a string, but above, it's a JSON Schema object.

External means the schema lives at a different location specified by a URL. This could be a link to a JSON Schema document, but above, it's a URL to an XML Schema Definition.

Frankly, I just didn't want to hand-type an XSD. :)

You can think of it like an HTML <script> tag, which has similar flexibility. I'd be open to changing src to href to match the rest of Siren.

@xogeny
Copy link
Contributor

xogeny commented May 29, 2017

OK, so you want to support any arbitrary schema. So you expect the client to determine the format based on actually accessing the schema (i.e., looking at the supplied value)? I ask because you didn't include a field to represent the specific content type associated with the schema, e.g., application/schema+json.

@kevinswiber
Copy link
Owner Author

kevinswiber commented May 29, 2017

@xogeny Ah, yes. I intended to include that, but it was a 3AM kinda thing, so it slipped by. :)

A few changes below. I changed src to href. I added type. I think the rules should state schema MUST contain either href or value (mutually exclusive), and type SHOULD be included.

EDIT: And of course, this is a type hint. The client needs to be prepared to handle whatever the response is in a way that makes sense to the User Agent.

Inline:

"actions": [
  {
    "name": "addNewProduct",
    "title": "Add a new product (lots of structured data required)",
    "method": "POST",
    "href": "http://api.x.io/products",
    "type": "application/json",
    "schema": {
       "type": "application/schema+json",
       "value": {
         "$schema": "http://json-schema.org/draft-04/schema#",
         "title": "Product",
         "description": "A product from Acme's catalog",
         "type": "object"
        }
      }
    }
  ]

External:

"actions": [
  {
    "name": "addNewProduct",
    "title": "Add a new product (lots of structured data required)",
    "method": "POST",
    "href": "http://api.x.io/products",
    "type": "application/xml",
    "schema": {
       "href": "http://api.x.io/schemas/productSchema.xsd",
       "type": "application/xml"
      }
    }
  ]

@xogeny
Copy link
Contributor

xogeny commented May 29, 2017

OK, several questions.

Are fields and schema mutually exclusive? Or would you want the option to provide both in parallel? (giving the client the option?).

If you really want to allow both fields and schema, then that naturally begs the question, what if I want to provide multiple schemas (e.g., both XSD and JSON schema)?

I see at least two philosophical approaches here. One could say "sure, let's make schema an array instead" and allow as many as the client wants. I'm guessing that most people will feel like this would make handling of actions way too complicated. I, for one, don't really need this complexity.
But if we only allow one schema per action and just tell the client "this is the schema I've got, sorry if you don't like it" then I still think we still have an issue. Specifically, this issue of fields vs schema. So in that case I'm wondering...can we actually combine them?

One thing that makes me a bit nervous in this who schema discussion is what is the meaning of type in the action. It seems to me that we are overloading it. As I saw it, what it used to mean was "this is the media type that my payload should have" (e.g., JSON, XML, URL encoded). This is not the same thing as saying how you want to define the structure of your payload. I could have a schema that is in JSON Schema but the action requires that I submit it as URL encoded. So perhaps we want to actually have to fields, type (for what format the submitted payload should be in) and schemaType to let the client know how to parse our schema definition?

So assuming we resolve that issue, it seems to me that fields is overlapping with schema in so much as the fields still just represents a specific schemaType. In other words, one could (I'm not necessarily proposing this), actually deprecate fields in favor of:

   "schemaType": "text/html",
   "schema": {
      "fields": [
        { "name": "orderNumber", "type": "hidden", "value": "42" },
        { "name": "productCode", "type": "text" },
        { "name": "quantity", "type": "number" }
      ]
    }

The reason for doing this is that otherwise, you end up (potentially) in a situation where you say "for simplicity's sake, you can only tell the client about one schema format...uh...unless you want to specify both fields and schema". But making fields just another schema type could be seen as a unification of fields and schema so the semantics were more consistent.

Those are some thoughts. I'm not trying to be difficult, I just want to point out some potential inconsistencies with the hope that we can resolve them in such a way that the resulting specification is cleaner.

@kevinswiber
Copy link
Owner Author

The type keyword is already overloaded. It's the equivalent of an HTML enctype in actions. For links, it's a hint on the media type that will be returned when following the href. I think, in context, it's not too difficult to figure out. I opt for shorter names, and that comes at some expense.

Yes, I have been thinking of fields and schema as being mutually exclusive. Fields are less strict and have richer semantics than any schema definition I've seen, though arguably, JSON Schema Validation gets close on the semantic aspect.

I'm hesitant to make schema an array and wonder if it would make sense to have a media type that lists schema options instead. A Siren action could point to the multi-select schema media type, and the client could choose from there. It's worth noting that currently, Siren actions only allow one type per action.

As always, I appreciate and welcome competing points of view. I never dismiss it as "being difficult." The subject, itself, is difficult, which is why analyzing various points of view is necessary.

Unifying fields and schema might have a certain tidiness, but as a breaking change, it comes at quite a high cost. I'd be fine leaving them mutually exclusive.

@mcintyre321
Copy link

mcintyre321 commented May 30, 2017 via email

@kevinswiber
Copy link
Owner Author

@mcintyre321 Good point on content negotiation. XML Schema Definitions don't have a specific media type (they use application/xml or text/xml), which is problematic but not a huge deal. I mean, how many schema types do we really use for APIs anyway? I think conneg is a good solution.

The schema type would be optional, for sure.

@xogeny
Copy link
Contributor

xogeny commented May 30, 2017

As far as breaking changes w.r.t. fields, another option could be to actually use fields to hold the schema. If it is an array, treat it as an existing fields definition. If it is a string, treat it as a link to the schema definition (using content negotiation as a guide). If it is an object, treat it as an inline definition. Backward compatible and covers all the variations discussed so far (except inline XSD...but honestly...that would be pretty revolting and this would encourage people to do The Right Thing and simply link to it).

@kevinswiber
Copy link
Owner Author

@xogeny Can you type out an example?

I thought about this, but I'm not sure how to make it un-confusing. Would it look something like this?

"fields": [
  { "type": "schema", "schemaType": "application/xml", "href": "https://..." }
]

Or:

"fields": [
  {
    "type": "schema",
    "value": {
       "$schema": "http://json-schema.org/draft-04/schema#",
       "title": "Product",
       "description": "A product from Acme's catalog",
       "type": "object"
      }
  }
]

Feels a little inside-out, IMHO.

@xogeny
Copy link
Contributor

xogeny commented May 30, 2017

Sure.

Current (HTML fields)

"actions": [
  {
    "name": "addNewProduct",
    "title": "Add a new product (lots of structured data required)",
    "method": "POST",
    "href": "http://api.x.io/products",
    "type": "application/xml",
    "fields": [
        { "name": "orderNumber", "type": "hidden", "value": "42" },
        { "name": "productCode", "type": "text" },
        { "name": "quantity", "type": "number" }
      ]
}

Inline

"actions": [
  {
    "name": "addNewProduct",
    "title": "Add a new product (lots of structured data required)",
    "method": "POST",
    "href": "http://api.x.io/products",
    "type": "application/xml",
    "fields": {
       "$schema": "http://json-schema.org/draft-04/schema#",
       "title": "Product",
       "description": "A product from Acme's catalog",
       "type": "object"
      }
}

External

"actions": [
  {
    "name": "addNewProduct",
    "title": "Add a new product (lots of structured data required)",
    "method": "POST",
    "href": "http://api.x.io/products",
    "type": "application/xml",
    "fields": "http://api.x.io/schemas/productSchema.xsd"
}

I ditched the type specifier for a couple of reasons. First, I'm a bit uncomfortable about the subtle overloading of type (between payload content type and schema format). But second, I'm not sure it is even necessary because if it is an array, you know the type. If it is a string, you get the type by content negotiation. If it is an object it could, in theory, be any kind of schema. But the thing is, if it is inline it would have to be JSON. If it is JSON, chances are almost certain it is JSON Schema. You could have other types of inline formats besides JSON schema. But in any case, one would hope that the shape of that schema is such that the client could infer exactly what it was. Most clients wouldn't even go that far. Instead, they would just check to see if it was a format they understood or not (no need to infer formats you don't even understand). Honestly, your use of $schema pretty much does the job of that type anyway.

That is what I had in mind at least. Backward compatible, compact and covers all cases discussed so far except inline XSD. Again, just a thought.

@Kampfgnom
Copy link

Just to add some thoughts: Concerning external schemas I would in my current projects most likely go with using techniques that the schema format already provide i.e. the $ref key for JSON schema:

"actions": [
  {
    "name": "addNewProduct",
...
    "fields": {
       "$schema": "http://json-schema.org/draft-04/schema#",
       "$ref": "http://schemas.host.com/product.schema.json"
      }
}

I do not know it there are similar ways for other schema formats, which is why I think you still need other means of linking to external schemas in the Siren spec directly.

@MathiasReichardt
Copy link

I would love to use an official solution to provide JsonSchemas via Siren.
Are there some further thougths on this topic? Great discussion so far.

I kind of like the schema property a bit more. The name and meaning does not collide with fields and it is pretty explicit. About backward compatibility i'm not so sure.

@Kampfgnom
Copy link

Kampfgnom commented May 7, 2018

I would love to resurrect this topic 🙂

We have requirements that are a bit more simple than @xogeny's and would love to get them into the standard. Namely, we only need to support application/json (and derivations) in conjunction with JSON Schemas.

After all those years, I believe that those requirements are best fulfilled by using a separate schema key on the action object, as @MathiasReichardt suggested.

My suggestion is as follows:

  1. fields are left unchanged
  2. add new key schema, that has to be a valid JSON schema object
  3. fields and schema can be present at the same time. The implementer has to make sure that they are equivalent, if that's what he needs.
  4. For me schema implies a type that is compatible to application/json (e.g. application/vnd.siren+json 😉 ), but that's probably best left to the implementer.

Examples:

{
  "actions": [
    {
      "name": "htmlStyleAction",
      "fields": [
        { "name": "field1", "type": "text" },
        {
          "name": "field2",
          "type": "checkbox",
          "value": [{ "name": "value1" }, { "name": "value2" }]
        }
      ]
    },
    {
      "name": "inlineSchemaAction",
      "schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "$id": "https://schema-host/my.schema.json",
        "type": "object",
        "properties": { "field1": { "type": "string" } }
      }
    },
    {
      "name": "referencedSchemaAction",
      "schema": {
        "$schema": "http://json-schema.org/draft-04/schema#",
        "$ref": "https://github.com/kevinswiber/siren/blob/master/siren.schema.json"
      }
    },
    {
      "name": "mixedSchemaAction",
      "schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "$id": "https://schema-host/my.schema.json",
        "type": "object",
        "properties": { "field1": { "type": "string" } }
      },
      "fields": [{ "name": "field1", "type": "text" }]
    }
  ]
}

@kevinswiber Are you interested in a PR for this?

In case you want to see real-world behavior first, I would love to hear your opinion first, since it will be some effort to get this implemented in my current project. It would be much easier to convince people of the implementation if you give your blessing ;)

Concerning XML etc

I completely understand that a true hypermedia API should take more formats than just JSON into consideration, but I suggest that for the sake of incremental improvements you only standardize JSON schema for now. By adding the schema key as an object, you can easily extend that later on and the standard finally continues evolving.

I have the bad feeling that the Siren world is quite fragmented already, and by standardizing stuff that's already in use out there, everyone would benefit. You my two cents 🙂

@xogeny
Copy link
Contributor

xogeny commented May 7, 2018

@Kampfgnom Looks very nice to me. The fact that you use a previously unused keyword everywhere and didn't change any existing ones means, if I understand your proposal correctly, that it can be implemented as a pure extension to existing Siren.

Note, I'm not implying it should remain that way. I think we really do need a richer way of specifying the underlying data model.

As for XML, my concern previously was trying to avoid the current issue of effectively only supporting HTML forms. Expanding it as you have outlined means that it then becomes HTML forms + JSON and that further extensions would be required for actions that POST, for example, XML data. Personally, I never use XML and I build all the APIs I use myself. So I'm fine with what you are proposing. I'm just noting that it would require further "extensions" for users who wanted to support other formats (e.g., XML, protobuf, etc).

@MathiasReichardt
Copy link

@xogeny I understand/share your concern about multiple schemas. On the other hand I requested application/vnd.siren+json in the first place, so it seems fair and reasonable that the API responds in the format (including schema) originally requested (+json).

@Kampfgnom I like your layout, especially that inline and referenced schemas are possible (which is great for client side caching). This is what I need to feed schemas directly to form generators.

Another solution would be to tell the server about the desired schema format, i did tell him I want Siren, so why don't tell him I want json schemas :)
Sadly the media type is so static and it is not feasible to register new ones all the time application/vnd.siren+json+json-schema-d-07

@apsoto
Copy link

apsoto commented May 7, 2018

@Kampfgnom IMHO 'standards' should come from (multiple) real world implementations and seeing what works and what doesn't, not from being 'blessed first' then implemented.

That said, if your use case has a need for a non-standard extension you should do it to fit your needs (and document the pros/cons for the community). There are no standards police going to show up and yank your ability to use Siren ;)

@xogeny
Copy link
Contributor

xogeny commented May 7, 2018

Speaking of implementations, perhaps it is worth sharing something I've been working on because it may provide a means for exploring this topic but without necessarily changing Siren.

Obviously, I've been on the "richer schema for actions" band wagon for a while. To me, schemas provide an important means for validating data as well as form generation. So I'm all for it. But one downside worth mentioning in this discussions is the degree of redundant information potentially presented by the Siren server. While it is true that a schema can be represented as a reference (thus minimizing the number of characters required to convey the structure of an action payload/URL), it really is static information that need not be bundled with a response.

Note that it isn't just the fields. Basically, all the fields associated with an action are typically static and the only "dynamic" aspect (the thing that might change from payload to payload) is whether a given action is "available" (whether that affordance is permitted by the current client).

But once you start thinking along these lines, there are a couple of other interesting things to note. For example, there is no way to convey what to expect in the properties of a resource. Similarly, there is no way to convey what relations you might find associated with a given resource and what resources those are likely to point to (and how many there might be).

Again, much of this is static. In fact, many approaches like OpenAPI/Swagger have up front specifications for this "static" information. But those formats do not necessarily lend themselves well to hypermedia APIs and tend to be more "CRUD" oriented with fixed resource URL patterns, etc.

After RestFest in Grand Rapids, I started playing around with the idea of writing up specifications for hypermedia (but in my case pretty Siren-centric) specifications for APIs. The idea is that a siren response could come with a simple Link header or a rel="describedby" relation that points to a profile that describes the API. But to be honest, I my main use case doesn't actually involve dynamically inspecting responses and then fetching profiles. Instead, my idea is more about using an up front specification to specify the classes and relations involved and then generating a bunch of stuff.

At the moment, I've got a prototype that can generating TypeScript type definitions for properties of resources and action payloads automatically from the specification. The definitions can be shared by both the client and server to ensure that the payloads being exchanged over the wire conform to a common "shape".

I'm also able to generate documentation that automatically generates a graph (both per resource and for the entire API) that shows the resources types, resource properties, actions, what relations exist between resources and the cardinality of those relations.

The next thing I'd like to add is to generate (per API) a client navigation library specific to that API. This library would be generated from the specification, include static types for all navigation (checking payloads of all requests and types of responses) and it would allow navigation using the resources and relations named in the API, e.g.,

let orders = await api.findCustomer({ name: "ACME" }).follow("orders");
orders.sort((a, b) => a.orderData > b.orderDate);

This would all be Siren payloads behind the scene and, again, typings would ensure that everything here conforms to the expected data being passed into and back from the API. I already have a library called siren-nav that allows navigation through a generic Siren API. The code generation would simply build on this to provide something using specifically the resources, relations and actions that were part of the specification.

Part of the plan here is that during development, validation checks would be put in place during testing to ensure that payloads being passed around conformed to the schemas specified. Furthermore, the profile already allows resources, relations and actions to be marked as deprecated allowing further checks if you want to have a strictly conforming interaction with the API (i.e., no deprecated calls).

Why am I mentioning all this in this thread? Well in my use case, all this schema stuff is kept in the profile and the action responses are minimal (the information simply isn't needed if the client was constructed from the profile...it already knows all this). The bottom line here is that this project of mine is currently in a private repository. But if people are interested in this approach, let me know and I can make it public and we can perhaps move this discussion over there and all get out of @apsoto's hair. 😄

@MathiasReichardt
Copy link

MathiasReichardt commented May 7, 2018

Swagger is in deed CRUDy. But I recently learned that links are introduced in v 3.0 which is good. Graphs and linked types/classes, delivered by the API seem a interesting approach. There is also a danger in becoming the new WSDL though :)

I did also work on a prototype for a type save client, which would fail early. I (hand wrote) some client side classes for responses and pass it to a generic client. It looks something like this:

var hypermediaObjectRegister = CreateHypermediaObjectRegister();
var sirenClient = new SirenHttpHypermediaClient<EntryPointHco>(ApiEntryPoint, hypermediaObjectRegister);
var customersAll = await sirenClient
    .EnterAsync()
    .NavigateAsync(l => l.Customers)
    .NavigateAsync(l => l.All);

var customer = customersAll.Customers.First();
var actionResult = await customer
    .CustomerMove
    .ExecuteAsync(new NewAddress {Address =  "New Address"});
Assert.IsTrue(actionResult.Success);

Experimental Siren client

Would realy like to continue discussing about discoverable documentation/schemas and clients.

@Kampfgnom
Copy link

Just to add to the "existing implementations" part: Our current way of working involves hand writing a small wrapper client around a more general siren client, that only encodes relation- and action-names. The resulting code looks like this:

const buildings = await client
  .entryPoint(context)
  .generalManagement()
  .findBuildingsOfCompany({ customerNumber: '123'  })
  .items();

That being said, I acknowledge everything said about specifications and generation of clients. But I would like to come back to the "incremental improvements"-part 😉 At the same time I acknowledge @apsoto's comment about "multiple real-world implementations".

All in all I'm happy to see that our current approach is at least "not bad"™️ . I will now incorporate it into a part of our solution and we'll see how that goes. Since I am working for a big'ish customer, I hope that we all can get some meaningful result from that 🙂

@xogeny
Copy link
Contributor

xogeny commented May 8, 2018

@Kampfgnom and @MathiasReichardt It isn't clear to me from your responses whether you are interested in looking at this profile specification stuff. If so, I'll write something up about it and publish it. Just let me know. My email is associated with my GitHub ID, so just reach out to me there so we don't take up any more space in this thread about that.

@vasilakisfil
Copy link

vasilakisfil commented May 9, 2018

@xogeny I have some questions. I have to say that I find very interesting what you are describing and the general discussion here, but I want to make sure that I have understood completely what you describe.

While it is true that a schema can be represented as a reference (thus minimizing the number of characters required to convey the structure of an action payload/URL), it really is static information that need not be bundled with a response.

I would say that metadata (hypermedia etc) is relatively much less volatile than the data. By having them through a reference link, you can add whatever caching headers you want through HTTP, so it doesn't always need to be bundled with a response. Most of the times, you just need to fetch it once.

But once you start thinking along these lines, there are a couple of other interesting things to note. For example, there is no way to convey what to expect in the properties of a resource. Similarly, there is no way to convey what relations you might find associated with a given resource and what resources those are likely to point to (and how many there might be).

If I understand you correctly, we don't have such spec as of now for associations etc, that doesn't mean that it's not possible through those "rich schemas". Personally I would love to have such specs ready but it's not easy and in my opinion ideally we should opt for compsability, i.e. compose different specs together instead of creating a complex monolith that tries to solve everything at once. But that's another discussion..

Again, much of this is static. In fact, many approaches like OpenAPI/Swagger have up front specifications for this "static" information. But those formats do not necessarily lend themselves well to hypermedia APIs and tend to be more "CRUD" oriented with fixed resource URL patterns, etc.

I never liked Swagger/OpenAPI for that reason: their thinking is to create the specs offline and from that go and scaffold code and possibly documentation. If Swagger allowed me to expose the API specifications online in a standard way, without any change I think it would be fine (maybe they can do that now? I haven't touched it for a while). If that was possible, then we could talk about patterns, efficiency and good design for exposing those metadata, compare and contrast with your (or anyone else's) solution.

The idea is that a siren response could come with a simple Link header or a rel="describedby" relation that points to a profile that describes the API. But to be honest, I my main use case doesn't actually involve dynamically inspecting responses and then fetching profiles. Instead, my idea is more about using an up front specification to specify the classes and relations involved and then generating a bunch of stuff.

Curious: how is the describedBy structured? Does it describe the endpoint, the resource or the whole API?

Also, from what I understand, the difference in your implementation compared to conventional hypermedia-based API-agnostic clients is that you first fetch the metadata of the API (hypermedia, fields, types, associations etc) and then you do the API requests (after you build your client). Am I right ? If that's the case, given that the API design allows you to do such thing, I am pretty sure that's the best practice actually and not an edge case.

Again if I have understood you right, I don't feel that this is much different from Swagger, only that is done the right way. As I said before, unfortunately Swagger focused on scaffolding stuff rather than the self-discovery of API metadata/hypermedia through API introspection.

Why am I mentioning all this in this thread? Well in my use case, all this schema stuff is kept in the profile and the action responses are minimal (the information simply isn't needed if the client was constructed from the profile...it already knows all this).

I am wondering how you have structured all those metadata (I feel metadata is the right word here and not hypermedia as hypermedia is a subset of metadata and mostly refers to links and actions. Correct me if I am wrong). Would you like to share more information ? If I can do a high level overview: what you are describing isn't a meta-API for your API that you exploit to setup your client beforehand ? How is that different/similar to a generic API introspection (regardless the specs/profiles used)?

@kevinswiber I like the initiative here, but I would like to be able to negotiate for the schemas that I understand. And I don't mean XML vs JSON Schema, but also negotiate for which version of JSON Schema. However, I don't think that's Siren's responsibility and should left to HTTP. But I feel that this is gonna be super complex with proactive-negotiation (I have tried such discoveries with similar things but it's a chicken-n-egg problem in proactive negotiation..) and reactive negotiation would help a lot. So I guess I don't help you much here but if you could find a way to support that it would be awesome :)

@xogeny
Copy link
Contributor

xogeny commented May 9, 2018

OK, I think we've gone too far beyond the scope of this issue. I've inadvertently hijacked this whole thing. To put an end to that, I've made my current work in progress public here:

https://github.com/xogeny/hyprofile

I ask that if @vasilakisfil, @MathiasReichardt and @Kampfgnom (and anybody else) have comments or questions, they log them as issues in that repo. Thanks!

@samfrances
Copy link

Is there any progress on this idea?

@mtiller
Copy link

mtiller commented Jan 15, 2021

I just wanted to drop a note in here about this topic 4+ years on. I contributed to this thread previously as @xogeny but as that company has since been acquired, I'm just commenting under my personal Github account.

I wanted to try and stay as conformant to Siren as I could. I ended up handling this in effectively two ways. The first was to establish a profile relation link to my resource that includes in it the collection of JSON schemas for each supported action. Furthermore, I hope to further add JSON schemas for classes associated with the resource (the union of which would presumably count as a JSON schema for the properties themselves, at least the way I approach the API design). To summarize, the profile would ideally return something like this:

{
  ...
  schemas: {
    classes: {
      "className1": { /* JSON Schema for class named "className1" */ },
      ...
   },
   actions: {
     "actionName1": { /* JSON Schema for action named "actionName1" */ },
     ...
  }
}

The nice thing about what I've described so far is that it doesn't require changing the Siren specification. Furthermore, I see the profile information as essentially static. Because it is accessed independently of the resource itself (i.e., has its own URL), I can put very different cache-control headers on it (presumably ones that allow the browser to hang onto this information for extended periods of time in the case of mature APIs). In practice, this means that most applications would generally only have to truly GET this information once.

But I also added a schema field to the action. This gives a more direct indication that this particular action has a schema associated with it. But to avoid including enormous blobs of schema for all clients, I instead rely on the $ref feature of JSON schema so it adds just a single line, e.g.,

"schema": { "$ref": "/profile/url/for/resource#/schemas/actions/add" }

Now, I don't actually expect to use the schema field myself. The reason is mainly because I access my APIs via my siren-nav library. That, in turn, leverages my siren-types library which defines an Action as:

export interface Action {
  name: string;
  class?: string[];
  method?: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
  href: string;
  title?: string;
  type?: string;
  fields?: Field[];
}

Note the absence of a schema field. Again, I'm trying to stick with standard Siren here and I don't want to extend my siren-types library to include fields that are not part of the Siren spec. A user could always do some kind of dynamic cast if they knew the field was there. But I don't want to confuse users of that library into thinking that schema is part of Siren by referencing it here.

So, in practice what I will end up doing is that for APIs that support the special profile link that yields schemas, This means of accessing the schema can already be expressed in my siren-nav library quite easily as: nav.follow("profile").get().asJSON() and then simply extract the field schemas.actions["<action-name>"] from the JSON that is returned.

Now, all that being said, I do think it would be nice to have a standard field in the action itself. It just seems like a nice way to extend the self describing nature of Siren a bit more. Frankly, there are rarely applications where I can use the HTML-ish field descriptions that are standard in Siren and even in those cases where I do, using libraries like typebox I can really easily provide such schemas. This is important in practice (especially in APIs) because it allows me to have a "single source of truth" about the data I'm working with can support:

  • Specification of static types for type checking of code
  • Validation of deserialized data (to ensure it conforms to the static type shape)
  • Ability to serialize the type itself (into JSON schema) and share it with clients (i.e., the schema field) to allow client side validation as well.

Anyway, I just wanted to share this with others. It doesn't really negate what I think is still an open need for a schema field, but I wanted to point out how I chose to work within the constraints of existing Siren to arrive at a satisfactory (IMHO) solution.

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

9 participants