Parsing JSON into variant based on the data shape

I’m writing a WebSocket library with variant type parameters (<req, res>). And when using JSON.parse for the data on the message event, I get an object with no TAG. This causes pattern matching on the result doesn’t work.

How to parse (classify?) serialized JSON into variant based on the data shape?

Below is a minimal example of my issue, where the code inputs “one” while I need it to input “two”.

type t =
  | One({a: int})
  | Two({b: int})

@bs.scope("JSON") @bs.val
external parse: string => t = "parse"

switch parse(`{"b": 2}`) {
  | One(_) => "one"->Js.log
  | Two(_) => "two"->Js.log
}

I believe you will need to use some JSON parsing functions to help.

Some available libraries are:

There was also a recent thread about JSON parsing this that may help.

I hope that helps get you started.

2 Likes

The JS representation of a variant is internal, and you should not rely on its structure. You’d need to serialize the variant yourself into a json compliant value, so you can later on restore the structure on the other side.

For quick and dirty serialization / deserialization, there’s a function called Js.Json.serializeExn and Js.Json.deserializeExn that can help quickly convert any data structure to a string and vice versa.

Here is an example:

module Data = {
  type t =
    | One({a: int})
    | Two({b: int})

  let toJsonString = (t: t): string => {
    Js.Json.serializeExn(t)
  }

  let fromJsonString = (str: string): t => {
    Js.Json.deserializeUnsafe(str)
  }
}

// Serializing the data and send it over the wire
let incoming = Data.toJsonString(One({a: 1}))

// Receive the string and deserialize it again
let data = Data.fromJsonString(incoming)

Js.log(data)

switch data {
| One(_) => "one"->Js.log
| Two(_) => "two"->Js.log
}
4 Likes

@kevanstannard thank you for the links, got a much better idea about what options are out there.

@ryyppy Thank you for the code example, and Js.Json.serialize... / deserialize... is a good option. Unfortunately, I’m writing a lib for some public WS API and don’t have control over how the data on the sender’s end is serialized, all I get is serialized JSON. That said Js.Json.deserialize... fits my case better than Js.Json.parse.

Overall it seems like I was overly optimistic about auto-deducting a variant option from the JS object shape. And many good options don’t work for me because:

  • I don’t control serialization on the sender’s end;
  • and I don’t know the shape of the data I’m trying to decode ahead of time, since the current message could be one of many types.

I ended up with something pretty basic and not very safe, but it works:

type t =
  | One({a: int})
  | Two({b: int})
  | Unknown

let msg = Js.Json.deserializeUnsafe(`{"b": 2}`)

let myData = if msg["a"] {
  One({a: msg["a"]})
} else if msg["b"] {
  Two({b: msg["b"]})
} else {
  Unknown
}

switch myData {
| One(_) => "one"->Js.log
| Two(_) => "two"->Js.log
| Unknown => "unknown"->Js.log
}

So does that mean that the sender is not using serializing ReScript related data structures over the wire?

In that case you are not supposed to use Js.Json.deserializeUnsafe, because this function should only be used in case the data was serialized with Js.Json.serializeExn. The purpose of these two functions is to loosely translate internal data structures to a JSON compatible data format.

So in case your WS server only sends you plain JSON that is unrelated to any ReScript data structures, you need to either formally parse and decode the JSON, or you go the unsafe route by making type assumptions on the data boundary:

type t =
  | One({a: int})
  | Two({b: int})
  | Unknown

module Decode = {
  external convertMsgUnsafe: Js.Json.t => 'a = "%identity"

  let unsafe = (json: Js.Json.t): t => {
    let msg = convertMsgUnsafe(json)

    if msg["a"] {
      One({a: msg["a"]})
    } else if msg["b"] {
      Two({b: msg["b"]})
    } else {
      Unknown
    }
  }
}

let msg = Js.Json.parseExn(`{"b": 2}`)->Decode.unsafe

switch msg {
| One(_) => "one"->Js.log
| Two(_) => "two"->Js.log
| Unknown => "unknown"->Js.log
}

which compiles to some pretty lean JS:

// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';


function unsafe(json) {
  if (json.a) {
    return {
            TAG: /* One */0,
            a: json.a
          };
  } else if (json.b) {
    return {
            TAG: /* Two */1,
            b: json.b
          };
  } else {
    return /* Unknown */0;
  }
}

var Decode = {
  unsafe: unsafe
};

var msg = unsafe(JSON.parse("{\"b\": 2}"));

if (typeof msg === "number") {
  console.log("unknown");
} else if (msg.TAG) {
  console.log("two");
} else {
  console.log("one");
}

exports.Decode = Decode;
exports.msg = msg;
/* msg Not a pure module */
5 Likes

@ryyppy a bit late, but just wanted to say big thanks for nudging me in the right direction.

Why this function not documented?