Using bs.deriving abstract for parsing JSON Responses

interop

#1

I always used bs-json library to convert Json results from Fetch into ReasonML records. Actually, this is good, as I get runtime validations using the same types, as during compilation time. But this requires a lot of code to encode/decode fields.

So, I decided to try [@bs.deriving abstract]: https://bucklescript.github.io/docs/en/object.html#creation and have issues correctly using it in Fetch promises.

Giving such abstract types:

[@bs.deriving abstract]
type tIssue = {
  author: string,
  authorLink: string,
  summary: string,
  linkToArticle: string,
};

[@bs.deriving abstract] **/* btw, how can I declare abstract arrays? :-| */**
type arrayOfIssues = {value: array(tIssue)};

I try to use constructors for ‘tIssue’ and ‘arrayOfIssues’ to get my objects. But these constructors require labeled arguments for each of type fields.

How can I send ‘result’ from following code into constructor?:
Js.Promise.(
Fetch.fetch(
"/issues/" ++ string_of_int(issueNumber) ++ “.json”,
)
|> then_(Fetch.Response.json)
|> then_(result => {
let typedResult = tIssue(result) /* this does not work obviously */
self.send(
RenderIssue(arrayOfIssues(~value=[|typedResult|])),
);
Js.Promise.resolve();
})
|> ignore
)

If I try to access result##author…etc like this:
let typedResult = tIssue(~author=result##author…);

Then, the compiler gives me another valid error:

We’ve found a bug for you!
/home/gladimdim/projects/reasonmlonline/src/components/issue.re 35:18-47:19

33 ┆ )
34 ┆ |> then_(Fetch.Response.json)
35 ┆ |> then_(result => {
36 ┆ let typedResult =
. ┆ …
46 ┆ Js.Promise.resolve();
47 ┆ })
48 ┆ |> ignore
49 ┆ )

This has type:
Js.Promise.t(Js.t(({… author: string, authorLink: string,
linkToArticle: string, summary: string}
as 'a))) =>
Js.Promise.t(unit)
But somewhere wanted:
Js.Promise.t(Js.Json.t) => 'b

The incompatible parts:
Js.Promise.t(Js.t('a)) (defined as Js.Promise.t(Js.t('a)))
vs
Js.Promise.t(Js.Json.t) (defined as Js.Promise.t(Js.Json.t))

Further expanded:
  Js.t('a)
  vs
  Js.Json.t (defined as Js.Json.t)

ninja: build stopped: subcommand failed.

Finish compiling(exit: 1)

Is this [bs.deriving abstract] indended to only send data to JS world? And it was not designed to get some data from it?


#2

Is this [bs.deriving abstract] indended to only send data to JS world? And it was not designed to get some data from it?

To get the data from an abstract type you use the generated function that has the same name as the prop. So for example (removed all but one prop in your example to save space):

[@bs.deriving abstract]
type tIssue = {
  author: string
};
let issue = tIssue(~author="John");
let issueAuthor = author(issue); /* this function is same name as the prop in your abstract type*/

The generated functions live in the same scope as the type so be careful of conflicting names shadowing each other.

Also to make this work:

|> then_(Fetch.Response.json)
|> then_(result => {
let typedResult = tIssue(result) /* this does not work obviously */

Assuming Fetch.Response.json returns a Js.Json.t you can unsafely cast it like:

external jsonToIssue: Js.Json.t => tIssue = "%identity";

...

|> then_(Fetch.Response.json)
|> then_(result => {
let typedResult = jsonToIssue(result);

Although that would work gotta stress again it’s pretty unsafe!

For this:

[@bs.deriving abstract] **/* btw, how can I declare abstract arrays? :-| */**
type arrayOfIssues = {value: array(tIssue)};

Since arrays map to js arrays you don’t need to make the array abstract so you can write
type arrayOfIssues = array(tIssue);


#3

Wow, thank you! I’ve read the docs, but never thought of using this escape exit :smiley:

As for this:

in reality, ‘result’ is an array of tIssues (I simplified example a little bit), so I can just do it like this now:

external jsonToIssue: Js.Json.t => array(tIssue) = "%identity";

It is even possible to have nested abstract types: ReasonML Playground

[@bs.deriving abstract]
type tIssue = {
  author: string
};

[@bs.deriving abstract]
type newIssue = {
  id: int,
  originalIssue: tIssue
};

Your answers will definitely help me to reduce time in prototyping things. Thank you again! I still think, that using bs-json gives more benefits in a long-run. Also, when writing bs-json encoders/decoder, I have to think more deeply about data structures I have and about the meaning of life :slight_smile:


#4

So I compared such approach:

module Decode = {
    let decodeIssue = input : tIssue =>
      Json.Decode.{
        author: input |> field("author", string),
        authorLink: input |> field("authorLink", string),
        summary: input |> field("summary", string),
        linkToArticle: input |> field("linkToArticle", string),
      };
    let decodeIssues = input => input |> Json.Decode.array(decodeIssue);
  };

and with [@bs.deriving abstract].

I like first approach as it gives me ‘normal’ access to object properties like this:

issue.author, issue.authorLink, etc.

In Deriving abstract approach, I have to access these properties as authorLink(issue), which is not very convenient for me. There is also a chance of function name collisions.

Also, I am not sure what happens, when input JSON has a different structure, than abstract type. In bs-json Json.Decode case, it will give readable error message like “expected “author” as string but number found”.

Or maybe, I have just used to Encode/Decode approach :smiley: