ocaml-decoders icon indicating copy to clipboard operation
ocaml-decoders copied to clipboard

Dynamic decoding?

Open idkjs opened this issue 4 years ago • 6 comments

Is it possible to use this lib to dynamically decode json? That is, json you dont know the shape of prior to getting it?

Thank you.

idkjs avatar Jun 23 '21 09:06 idkjs

Yes, you can use one_of to try a bunch of different shapes in turn, or if the value of some field in your JSON tells you what shape to expect next, you can decode the field and then dispatch to decoders for each different shape. See the section in https://github.com/mattjbray/ocaml-decoders#complicated-json-structure about shapes for an example.

If you can tell me a bit more about your specific use-case I can try to help more.

mattjbray avatar Jun 23 '21 11:06 mattjbray

@mattjbray thanks for showing interest.

Im trying to re-impletent the kentcdodds/match-sorter lib in reasonml as a learning exercise.

The lib has to be able to take in various types of input and process them and you will never know exactly what it is beyond a list or an object array.

Here is an example of the types of input:

Details
const objList = [
  { name: 'Janice', color: 'Green' },
  { name: 'Fred', color: 'Orange' },
  { name: 'George', color: 'Blue' },
  { name: 'Jen', color: 'Red' }
];
const fruit = [ 'orange', 'apple', 'grape', 'banana' ];
const things = [ 'google', 'airbnb', 'apple', 'apply', 'app' ];
const otherThings = [ 'fiji apple', 'google', 'app', 'crabapple', 'apple', 'apply' ];
const aliases = [
  { aliases: [ { name: { first: 'baz' } }, { name: { first: 'foo' } }, { name: null } ] },
  { aliases: [ { name: { first: 'foo' } }, { name: { first: 'bat' } }, null ] },
  { aliases: [ { name: { first: 'foo' } }, { name: { first: 'foo' } } ] },
  { aliases: null },
  {},
  null
];
const icecream = [
  { favorite: { iceCream: [ { tastes: [ 'vanilla', 'mint' ] }, { tastes: [ 'vanilla', 'chocolate' ] } ] } },
  { favorite: { iceCream: [ { tastes: [ 'vanilla', 'candy cane' ] }, { tastes: [ 'vanilla', 'brownie' ] } ] } },
  {
    favorite: {
      iceCream: [
        { tastes: [ 'vanilla', 'birthday cake' ] },
        { tastes: [ 'vanilla', 'rocky road' ] },
        { tastes: [ 'strawberry' ] }
      ]
    }
  }
];
module.exports = {
  objList,
  fruit,
  things,
  otherThings,
  aliases,
  icecream
};

I was looking to see if there are other options to @glennsl/bs-json. I have jsonm working, but as the internet notes, its not the easiest thing to use.

Any ideas you might share will be greatly appreciated, sir. Thanks again.

idkjs avatar Jun 24 '21 09:06 idkjs

Sounds like a fun exercise!

So it looks like the value the user passes in the keys parameter tells us what shape of JSON to expect.

Here's one way you could do it:

module Matchers (D : Decoders.Decode.S)
  (* parameterising the decoders backend allows us to re-use 
     the same code in Bucklescript and in native Ocaml. *)
  = struct
  (** Represents an item in the input JSON list *)
  type candidate =
    { orig_json : D.value
          (** The original JSON item. We'll return this if any of the values match. *)
    ; strings : string list
          (** Strings extracted from the JSON item we'll match against. *)
    }

  (** Given a [key] like ["a.b.c"], decode the string at c.

      This decoder uses [D.maybe], so instead of failing when the JSON does not
      have the right shape, it will succeed with [None].

      To allow array indexing in the key (e.g. ["a.b.0.c"]), you can write a
      customized version of [D.at] that checks whether an item in the path is an
      integer, and then uses [D.index] instead of [D.field].

      See https://github.com/mattjbray/ocaml-decoders/blob/e00bad1d2e4c2b1394aee9ae5a7a6fe3ab04ecec/src/decode.ml#L606
   *)
  let decode_string_at_key (key : string) : string option D.decoder =
    let path = String.split_on_char '.' key in
    D.maybe (D.at path D.string)


  (** Decode all the strings in an object following [keys]. *)
  let rec decode_strings ~keys () : string list D.decoder =
    let open D.Infix in
    match keys with
    | key :: keys ->
         decode_string_at_key key >>= fun str ->
         decode_strings ~keys () >>= fun strs ->
        let strs = match str with Some str -> str :: strs | None -> strs in
        D.succeed strs
    | [] ->
        D.succeed []


  let decode_candidate ?keys () : candidate D.decoder =
    let open D.Infix in
    D.value >>= fun orig_json ->
    let strings =
      match keys with
      | None ->
          (* No keys passed, assume value is a simple string. *)
          D.maybe D.string |> D.map (function | Some s -> [ s ] | None -> [])
      | Some keys ->
          decode_strings ~keys ()
    in
    strings >>= fun strings ->
    D.succeed { orig_json; strings }


  let decode_candidates ?keys () : candidate list D.decoder =
    D.list (decode_candidate ?keys ())
end

(** Assuming you're using bucklescript - use Decoders_yojson.Basic.Decode or something if you're using native *)
module D = Decoders_bs.Decode
module Matchers_bs = Matchers (D)

let candidates ?keys (json : Js.Json.t) : (Matchers_bs.candidate list, D.error) result =
  D.decode_value (Matchers_bs.decode_candidates ?keys ()) json

mattjbray avatar Jun 25 '21 09:06 mattjbray

Wow. Thanks. Let me try this. Will revert with news.

idkjs avatar Jun 25 '21 10:06 idkjs

Im trying run through your examples to get familiar.

To do so dune utop . then add the code.

fork (update-ocaml)  dune utop .
──────────────────────────────
p version 2.7.0 (using OCaml v
──────────────────────────────

Type #utop_help for help about using utop.

─( 18:19:49 )─< command 0 >─{ 
utop # module D = Decoders_yojson.Basic.Decode;;
module D =
  Decoders_yojson.Basic.Decode
─( 18:20:04 )─< command 1 >────────────────────────────────────────────────{ counter: 0 }─
utop # 

I can add it to the Readme.md if you would like or just leave this here.

idkjs avatar Jun 25 '21 16:06 idkjs

So, I can't seem to run the examples from the Readme.md for the life of me. I will have to try again tomorrow. I learned a lot about it today and maybe I am tired. Still feels kinda clownish. https://github.com/idkjs/modern-ocaml-decoders

Sorry about the dune ignorance here. I dont really want to leave ocaml/reason land yet and well rescript. So I have had to start diving into dune. I have not got a hold of it yet. For example, I finally got most of the examples compiling here but I cant seem to run them in rtop. This lib is unbound for some reason. Will stay on it.

idkjs avatar Jun 25 '21 18:06 idkjs