Can I safely communicate with serialzed variants?

I found it to be a HUGE win to communicate between a web apps frontend and backend using ordinary variants. So far I haven’t encountered any problems, but I’m wondering if there could be any issues.

Here’s the types (note this is ALL possible responses from server side currently, whether its scalable I haven’t decided yet)

type authType = Success | Fail | Invalid | Needed | Ended | Active | Inactive
type errorType = NotFound
type dataType = Bookmarks(array<Bookmarks.t>)
type createType = Duplicate | Success
type recoverType = Sent | NotFound | Reset | BadToken | TooShort

@tag("state")
type t =
  | Auth(authType)
  | Error(errorType)
  | Data(dataType)
  | Create(createType)
  | Recover(recoverType)
  | Init
  | Loading

Here’s the server responding with some bookmark data. This is just basic express, which I’m assuming just serializes with JSON.stringify

res->Express5.sendResponse(Data(Bookmarks(bookmarks)))

Here is the client getting the data. You can see the rescript goodness is where we can directly switch on the response.

        let response = await Responses.fetchGet("/backend/bookmarks")
        let bookmarks = switch response {
        | Data(Bookmarks(bms)) => bms
        | _ => Responses.raiseError(response) // didnt get response we expected
        }

Here is Responses.fetchGet()

let fetchGet = async (url: string): t => {
  open Fetch
  let args: Request.init = {
    method: #GET,
    headers: Headers.fromObject({"Content-type": "application/json"}),
  }
  let response = await fetch(url, args)
  let response = await response->Response.json
  let response = response->jsonTot
  response
}

Here’s the cheat code that makes this all possible without having to validate anything

external jsonTot: JSON.t => t = "%identity"

If you are wondering I’m not that concerned with validating because I would be validating a variant I just requested that was created from the exact same source. I’m not expecting a man-in-the-middle attack breaking my serialized variants.

I’m also very curious if the internal representation of the variant would be stable enough to generate it from another language, like a golang backend.

Here is what it looks like with empty data FYI, looks a little “undocumented” to me

{"state":"Data","_0":{"TAG":"Bookmarks","_0":[]}}

Since v11, the runtime representation of variants is stable and part of the language, so in those terms it’s safe to use variants - we won’t be changing their runtime representation.

As to whether it’s suitable, as long as you’re only passing things that can be serialized with JSON (or whatever serialization you’re using), then it’s great. Also following rules for how to evolve your types and so on to be backwards compatible, but that seems out of scope for this discussion. Passing things that can’t be automatically serialized/parsed back (like Date.t) seems like the biggest risk.

2 Likes

I’d definitely recommend to use a (de)serializer/validator lib, it’s just way to easy to forget about the rules of JSON and slip an undefined, a date or a function inside those variants! It just took me a few days to introduce such bugs in my codebase and I’m the only dev, so I can’t believe this could scale at all.
Take a look at rescript-schema or others, it’s really easy to set it up.

3 Likes

The JSON serialization piece is a big thing to keep in mind. I’ve been hitting that with Remix. It’s fine with strings and numbers, but dates get converted to strings.

It would be nice to have a way to take a type and convert it to a serialized version of that type.