janestreet / ppx_yojson_conv

[@@deriving] plugin to generate Yojson conversion functions
MIT License
58 stars 8 forks source link

represent arbitrary keys record #11

Closed mudrz closed 2 years ago

mudrz commented 2 years ago

I have to interact with an API, that returns data with the following format:

"Users": {
  "0": { "Name": "A" },
  "1": { "Name": "B" },
  "2": { "Name": "C" },
  ...
}

essentially Users is an array encoded in JSON as a record, with indexes as keys (I can't change that format) -> the keys are unknown

Is there any way to represent this with ppx_yojson_conv ? For example to convert it to a hash table / map (string, user) Map.t where user is:

type user = {
  name: string; [@key "Name"]
}[@@deriving yojson]

Is it possible to 'ignore' the contents of Users? (using an opaque attribute doesn't seem to work since you have to specify the type)

hhugo commented 2 years ago

You can define you own, manually written, encoding / decoding function https://github.com/janestreet/ppx_yojson_conv/blob/master/test/ppx_yojson_test.ml#L485

mudrz commented 2 years ago

thanks @hhugo ! Not sure if there is a more elegant solution, but I ended up with something like this:

module Indexed_record : sig
  type 'a t = 'a Map.M(String).t [@@deriving yojson, sexp]
end = struct
  type 'a t = 'a Map.M(String).t [@@deriving sexp]

  let t_of_yojson of_yojson yojson =
    match yojson with
    | `Null | `Bool _ | `Int _ | `Intlit _ | `Float _ | `String _ | `List _
    | `Tuple _ | `Variant _ ->
        failwith "Invalid type"
    | `Assoc xs ->
        let xs = List.map xs ~f:(fun (k, v) -> (k, of_yojson v)) in
        Map.of_alist_exn (module String) xs

  let yojson_of_t to_yojson v =
    let xs =
      let v = Map.map v ~f:to_yojson in
      Map.to_alist v
    in
    `Assoc xs
end

with uses:

type user = {
  name: string; [@key "Name"]
}[@@deriving yojson]

type my_thing = {
    users: user Indexed_record.t; [@key "Users"]
}[@@deriving yojson]
mudrz commented 2 years ago

it's also convenient to provide alternatives

  module Indexed_list : sig
    type 'a t = 'a list [@@deriving yojson, sexp]
  end = struct
    type 'a t = 'a list [@@deriving sexp]

    let t_of_yojson of_yojson yojson =
      match yojson with
      | `Null | `Bool _ | `Int _ | `Intlit _ | `Float _ | `String _ | `List _
      | `Tuple _ | `Variant _ ->
          failwith "Invalid type"
      | `Assoc xs -> List.map xs ~f:(fun (_k, v) -> of_yojson v)

    let yojson_of_t to_yojson v =
      let xs = List.mapi v ~f:(fun i x -> (Int.to_string i, to_yojson x)) in
      `Assoc xs
  end
hhugo commented 2 years ago

@mudrz, that is what I had in mind. You can also use https://github.com/janestreet/ppx_yojson_conv_lib/blob/master/yojson_conv.mli#L119 to handle the error case.

mudrz commented 2 years ago

even cleaner, thanks again!

  module Indexed_list : sig
    type 'a t = 'a list [@@deriving yojson, sexp]
  end = struct
    type 'a t = 'a list [@@deriving sexp]

    let t_of_yojson of_yojson yojson =
      match yojson with
      | `Assoc xs -> List.map xs ~f:(fun (_k, v) -> of_yojson v)
      | _ ->
          Ppx_yojson_conv_lib.Yojson_conv.of_yojson_error
            "indexed_list_of_yojson: tuple list needed" yojson

    let yojson_of_t to_yojson v =
      let xs = List.mapi v ~f:(fun i x -> (Int.to_string i, to_yojson x)) in
      `Assoc xs
  end