I am trying to derive the following record type with yojson ppx, but it fails and I honnestly don’t understand why. I feel I need to deep dive into polymorphic variants and stuff but I would prefer not to…
Why is the compiler interpreting int as a function in that case ?
module StringMap = struct
include Map.Make(String)
let of_yojson json =
let conv p = match p with
(k, `Int v) -> (k,v)
| _ -> failwith "values must be of type int" in
match json with
`Assoc l -> of_list (List.map conv l)
| _ -> failwith "Only valide for int value type"
let to_yojson map =
let conv (k,v) = (k, `Int v) in
`Assoc (List.map conv (bindings map))
end
type t = {
m : string;
v : int StringMap.t;
} [@@deriving yojson]
let () =
let a = {|{"m": "value", "v": {"1":1,"2":2}}|} in
let b = Yojson.Safe.from_string a in
let m = StringMap.of_yojson b in
let j = StringMap.to_yojson m in
print_endline (Yojson.Safe.to_string j )
the compiler says :
File "bin/main.ml", line 34, characters 6-9:
34 | v : int StringMap.t;
^^^
Error: This expression should not be a function, the expected type is
'a StringMap.t
replacing the int by a variable type 'a makes t a variable type as well, type 'a t = ... but I then get en even more complex error message plus I really don’t need t to be variable (or I just need int StringMap.t)
Not sure, but I was able to get it to work by specializing the type a bit more:
module StringMap = Map.Make(String)
module StringInt = struct
type t = int StringMap.t
let conv = function
| k, `Int v -> k, v
| _ -> failwith "expected int values"
(* Note, just hacking it and wrapping the result in Ok.
In a real codebase we would handle the result properly. *)
let of_yojson = function
| `Assoc l -> Ok (l |> List.map conv |> List.to_seq |> StringMap.of_seq)
| _ -> Error "expected JSON object"
let to_yojson map =
`Assoc (List.map (fun (k, v) -> k, `Int v) (StringMap.bindings map))
end
type t = { m : string; v : StringInt.t } [@@deriving yojson]
This gives me the derived functions:
val to_yojson : t -> Yojson.Safe.t
val of_yojson : Yojson.Safe.t -> t Ppx_deriving_yojson_runtime.error_or
Need to make StringMap’s of/to yojson functions accept an argument to decode/encode its value type. This is because the type of a map is type 'a t, takes a type param and deriving expects that it can pass a decoder/encoder for each of a type param the type has.
module StringMap = struct
include Map.Make(String)
let of_yojson v_of_json json =
let conv (k, v) = (k, v_of_json v) in
match json with
`Assoc l -> of_list (List.map conv l)
| _ -> failwith "Only valide for int value type"
let to_yojson v_to_json map =
let conv (k, v) = (k, v_to_json v) in
`Assoc (List.map conv (bindings map))
end
I fully agree one need such functions, but assuming we do so, how would the yojson deriver manage this additionnal parameter when the StringMap has to be derived ?
type 'a t = { m : string; v : 'a StringMap.t } [@@deriving yojson]
(*<== where do I provide my value_to_yojson : 'a -> Yojson.Safe.t and yojson_to_value : Yojson.Safe.t -> 'a *)
type 'a t = { m : string; v : 'a StringMap.t } [@@deriving yojson]
so in this case a function of the following type will be generated:
val to_yojson : ('a -> json) -> 'a t -> json
which you then will use like this
let x : int t = ...
let json = to_yojson int_to_yojson x
It might be useful to check the generated by ppx code in the editor/IDE. I know VSCode has such functionality. If your editor doesn’t then change [@@deriving ...] to [@@deriving_inline ...] and observe the generated code pasted back after you do dune build --auto-promote
With your help, I finally reach the general case which I post below for others
open Ppx_yojson_conv_lib.Yojson_conv
module StringMap : sig
include Map.S with type key = string
val t_of_yojson : (Yojson.Safe.t -> 'a) -> Yojson.Safe.t -> 'a t
val yojson_of_t : ('a -> Yojson.Safe.t) -> 'a t -> Yojson.Safe.t
end = struct
include Map.Make(String)
let t_of_yojson value_of_yojson json =
let conv (k,v) = (k, value_of_yojson v) in
match json with
|`Assoc l -> of_list (List.map conv l)
| _ -> failwith "input must be of type `Assoc (string * Yojson.Safe.t) list"
let yojson_of_t yojson_of_value map =
let conv (k,v) = (k,yojson_of_value v) in
`Assoc (List.map conv (bindings map))
end
type toto = {
m : string;
v : int StringMap.t;
} [@@deriving yojson]
let () =
let a = {|{"m": "value", "v": {"1":1,"2":2}}|} in
let b = Yojson.Safe.from_string a in
let c = toto_of_yojson b in
let j = yojson_of_toto c in
print_endline (Yojson.Safe.to_string j )