Variant JSON vs Variant ReScript

I’m getting a strange runtime comparison issue. I defined a type like:

type method =
	GET
	| HEAD
	| POST
	| PUT
	| DELETE
	| PATCH
	| CONNECT
	| OPTIONS
	| TRACE

For my own sanity in log messages, I have a way to convert those back:

let methodToString = method => 
	switch method {
	| GET => "get"
	| HEAD => "head"
	| POST => "post"
	| PUT => "put"
	| DELETE => "delete"
	| PATCH => "patch"
	| CONNECT => "connect"
	| OPTIONS => "options"
	| TRACE => "trace"
	}

I then have some JSON I convert that may have that:

type jwtTokenPayload = {
	at: option<string>,
	ts: option<int>,
	m: option<method>,
	p: option<string>,
	h: option<Js.Array.t<string>>,
	hh: option<string>
}

@scope("JSON") @val
external jwkStringToJWK: string => jwk = "parse"

… and it’s pretty amazing. Coming from Elm where I have to write decoders, this is all like “boom, here’s your object, bruh”. Sick. HOWEVER… when I later go to compare, runtime oddness:

let methodMatches = (method, token) =>
	switch token.payload {
		| None => NoPayloadSoNoMethod
		| Some(payload) => switch payload.m {
			| None => NoMethodOnPayload
			| Some(m) => {
				Js.log2("ok, m is:", m)
				Js.log2("but method is:", method)
				Js.log2("ok, m is:", methodToString(m))
				Js.log2("but method is:", methodToString(method))
				if Js.String.toLowerCase(methodToString(m)) === Js.String.toLowerCase(methodToString(method)) {
					MethodMatches
				} else {
					MethodDoesNotMatch(`method: ${methodToString(method)}, token.payload.m: ${methodToString(m)}`)
				}
			}

Ok, those 4 log messages? Look at these bonkers outputs:

Js.log2("ok, m is:", m) // GET
Js.log2("but method is:", method) // 0
Js.log2("ok, m is:", methodToString(m)) // undefined
Js.log2("but method is:", methodToString(method)) // get

So m from JSON printing out as GET, while helpful, doesn’t follow the 8.2 docs (I’m using 9) stating it’ll generate JS to _0, _1, etc. But whatever, no big deal.

But method as 0 AND m is a problem. ReScript types both to method, yet at runtime, they’re different. So when I run === it fails… because GET doesn’t === 0… but… dude, how in the the, wat does this even mean?

Ok last one up is methodToString: his pattern match covers all angles… yet… he doesn’t because somehow it magically returns undefined? How is this even possible?

Hrm, this is odd. I made a function to help diagnose:

let methodIsGet = method =>
	switch method {
	| GET => true
	| _ => false
	}

And the m isn’t actually a GET. But it’s type is in the JSON parse; why does the compiler think it’s a type of method, but any of the variant types, like GET, it’s like “No no… it may be a method, but none of the ones you defined.” :thinking:

I guess m: option<method>, is, for now, actually m: option<string>,, eh? My compiler is saying it’s a method, but… well, we disagree, and that’s ok “because JavaScript”.

Yup, I just changed to string, changed a few lines, and things work now. Is this a compiler bug, or me stumbling into dangerous loosey goosey JSON decoding, or…

In this case, as you noted, the runtime value is the string "GET", so you need an appropriate way of representing that as a ReScript type.

Since ReScript polymorphic variants correspond to JavaScript/JSON strings, you can use those. E.g.,

switch payload.m {
| None => NoMethodOnPayload
| Some(#GET) => // GET
| _ => // other
}

The key here is that method is not a variant type as defined above; it’s a polymorphic variant type, which corresponds to a JSON string. So:

type method = [
| #GET
| #HEAD
// and so on
]

So what went wrong with methodToString(m)–why was it undefined? Well, the method type which was defined as a variant type (i.e. not a polymorphic variant type), and the runtime values of variant constructors let GET, HEAD, and so on are 0, 1, etc. You can check this in the ReScript playground or locally on your machine. Also the docs: https://rescript-lang.org/docs/manual/latest/shared-data-types

Variant. Check the compiled JavaScript output of variant to see its shape. We don’t recommend exporting a ReScript variant for pure JS usage, but you can do that if you have some interop needs.

So methodToString is basically checking whether the input is one of the numbers 0 to 8, and returning a corresponding string. But the m in that function call was the string "GET", so none of those paths were taken, so the function never returned. So it returned undefined.

1 Like