Deriving a record with a map field

Hi,

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

2 Likes

Your solution is what I was looking for since a few days…thank you !

Also I noticed (although @Khady already pointed it) that I had mixed ppx_yojson_conv and ppx_deriving_yojson …

1 Like

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 )