rescript-labs / decco

Bucklescript PPX which generates JSON (de)serializers for user-defined types
MIT License
227 stars 27 forks source link

How to represent nullable values? #62

Closed cevr closed 1 year ago

cevr commented 4 years ago
[@decco]
type project = {
  description: option(string),
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: option(primaryLanguage),
  isArchived: bool,
};

description and primaryLanguage will be decoded as undefined because they are either string | null. How do I write a decoder so that null values remain null?

ryb73 commented 4 years ago

Hi @cevr. I'm not sure I understand the issue. Can you include any error messages your seeing and/or expected function output compared to actual function output?

cevr commented 4 years ago

Hey @ryb73. Basically, the above will be decoded as such

let project = {
  description: undefined, // if null
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: undefined, // if null
  isArchived: bool,
};

This causes problems with next.js when it serializes the project object and sends it to the frontend.

The error is basically do not explicitly setundefinedvalues, rather set them asnullor omit them from the object

The desired output is

let project = {
  description: null, // if null
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: null, // if null
  isArchived: bool,
};

I tried

[@decco.decode]
type project = {
  description: Js.nullable(string),
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: Js.nullable(primaryLanguage),
  isArchived: bool,
};

But that isn't supported a supported, and I'm very lost on how to write a decoder for that.

ryb73 commented 4 years ago

Sorry, I'm still confused about your use case. Are you talking about encoding or decoding? Decoding is going from JSON to Reason, encoding is going from Reason to JSON. If you are decoding an option field, null will be decoded as None. If you encode an optionfield with a value of None, it will be encoded as null.

Could you post the specific code that you're running, the output you expect, and the actual output you're getting?

cevr commented 4 years ago

Ahh, I see.

I'm using this https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering to populate the props of a react component.

[@decco]
type primaryLanguage = {name: string};

[@decco]
type project = {
  description: option(string),
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: option(primaryLanguage),
  isArchived: bool,
};

[@decco]
type edge = {node: project};

[@decco]
type repositories = {edges: array(edge)};

[@decco]
type user = {repositories};

[@decco]
type response = {user};
let getServerSideProps = _context => {
  ProjectsApi.get()
  |> Js.Promise.then_((result: array(ProjectsApi.project)) => {
       Js.Promise.resolve(
         result->Belt.Array.keep(project =>
           project.stargazerCount > 1 && !project.isArchived
         ),
       )
     })
  |> Js.Promise.then_(result => {
       let props: props = {projects: result};
       Js.Promise.resolve({"props": props});
     });
};

The issue arises because the encoding and decoding are being handled internally by the framework I use (nextjs) when it injects props into a component.

So even if I manually encode it within getServerSideProps, it will not be option(string) but rather Js.nullable(string) because I cannot control the decoding when it gets to the component

So the answer again would be to support:

[@decco]
type project = {
  description: Js.nullable(string),
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: Js.nullable(primaryLanguage),
  isArchived: bool,
};

Does that make sense?

ryb73 commented 4 years ago

Ok, I think I see. What you could do is use the magic codec:

[@decco]
type project = {
  description: [@decco.codec Decco.Codecs.magic] Js.nullable(string),
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: [@decco.codec Decco.Codecs.magic] Js.nullable(primaryLanguage),
  isArchived: bool,
};

This passes through the value as-is without any checking.

cevr commented 4 years ago

ah perfect! Thank you :)

EDIT: Seems to error when used

We've found a bug for you!
/Users/admin/Projects/personal/me/src/api/ProjectsApi.re

This has type: Belt.Result.t('a, 'b) (defined as Belt_Result.t('a, 'b))
Somewhere wanted: Js.Json.t => 'c

FAILED: cannot make progress due to previous errors.
ryb73 commented 3 years ago

Sorry @cevr, I thought I'd responded to this. Thanks for catching that error, this is something I need to look into.

It's been a while so you've probably worked around this by now, but I think this is one way to work around the bug for now:

[@decco]
type primaryLanguage = {name: string};

type nullableString = Js.nullable(string);
type nullableLanguage = Js.nullable(primaryLanguage);

[@decco]
type project = {
  description: [@decco.codec Decco.Codecs.magic] nullableString,
  name: string,
  stargazerCount: int,
  url: string,
  id: string,
  primaryLanguage: [@decco.codec Decco.Codecs.magic] nullableLanguage,
  isArchived: bool,
};