-
Notifications
You must be signed in to change notification settings - Fork 48
optionalField Decoder #23
Comments
Hmm, that's an interesting case. I can definitely see how this can be generally useful, but I think I'd like to make errors more information-rich, but I'm not entirely sure how to best do so. Something like In the meantime, could you do something like this instead? exception Fatal(exn);
let fatal = decoder =>
json => try decoder(json) {
| DecodeError(_) as e => Fatal(e)
};
...
{ dateOfBirth: optional(field("date_of_birth", fatal(utcString))) } |
That's a good approach. We took a more manual work around, but this seems much cleaner. |
I think the definition of
Personally I would expect a decoder named Apart from the above, I'm really enjoying using this library ;) |
@glennsl Would you be open to a pull request switching |
@RasmusKlett Why not think of a new name? And mark the current one as deprecated? |
I do agree that the current situation is far from optimal, and that it will cause errors, but I think this needs to be thought through on a deeper level and be planned for a future 2.0 release. What do you mean by "switching Currently |
@ncthbrt You're right, that's probably a better strategy. @glennsl In my opinion the default I guess my suggestion is to have three separate decoders for these cases, so the decoders can be more strict in what they accept, warning the developer earlier when something is not as expected. |
@RasmusKlett Sounds good in theory, but how would you implement it? |
Warning: untested code ahead, I hope you get the point of it. Also I might be mixing Reason and OCaml syntax, sorry. The most often used I think would be the null-as-None decoder:
I guess to handle an undefined field, you would need a separate decoder: Lastly, we could have: This can be composed into
In my opinion this doesn't add too much syntactic noise, and it allows a much more granular specification of what we want, reducing surprising behaviour in the future by throwing early. |
@RasmusKlett Why not:
Seems like automatically wrapping in some would make it more composable |
|
Well yes, but I think returning Option instead of Js.null is crucial. After all, this library is for getting rid of the Js types, and getting nice OCaml types instead. Whether it is called
Good point that it only catches DecodeError, that should also be expressed. But my main gripe is exactly that the name is
I'm unsure what direction you want to generalize it in? I think if you want to generalize checking for undefinedness, you will have to change the definition of
I'm not sure if this is an improvement? In this way |
No...? It's for decoding json into a usefully typed data structure. Whether that structure should include js types or not is a choice left to the consumer.
I'm not a fan of redundant verbosity. That it's a decode operation is obvious from the context.
As it is now, every decoder follows the same simple "shape". They either decode successfully or they fail with an exception. By "general solution" I mean one which preferably conforms to this shape, or alternatively changes the overall shape into something that better captures the properties we need, but still remains uniform. I like |
@glennsl Well, I don't agree with a lot of your response. I guess I have different ideas for how a library like this should look. I'm glad you like the |
Hi, wanted to note here I just bumped my head against this issue too. |
If I could perform miracles I would, but unfortunately I cannot just will something like this into existence. If The So The documentation for
I'd happily consider suggestions for better formulations of course, but the underlying problem seems to be poor understanding of the primitives and basics of the library, or perhaps even language semantics, which is a non-trivial educational task. |
I've explored a bit more and found a solution that's promising, but probably can be teeaked a bit still, and might still have some unforeseen shortcomings. Interface: type field_decoder = {
optional : 'a. string -> 'a decoder -> 'a option;
required : 'a. string -> 'a decoder -> 'a
}
val obj : (field: field_decoder -> 'b) -> 'b decoder Usage: Json.Decode.(
obj (fun ~field -> {
start = field.required "start" point;
end_ = field.required "end" point;
thickness = field.optional "thickness" int
})
) Pros:
Cons(-ish):
|
@glennsl This looks like a promising approach. Looking at our decoder usage, it isn't very far removed from how your provisional design looks, but with a bit of extra verbosity introduced from the repeated field decoders and the application of the json object. |
Here's a few more considerations:
|
I just ran into this issue, too, as I was using The encoder val nullable : 'a encoder -> 'a option -> Js.Json.t turns an option into a JSON value, mapping So I would expect the But that's not the case, as the signature of the decoder is val nullable : 'a decoder -> 'a Js.null decoder |
As a point of interest, here is what we're using to solve the problem in our api endpoints: let optionField = (fieldName, innerDecoder, json) =>
Json.Decode.(
(
try (Some(field(fieldName, identity, json))) {
| Json.Decode.DecodeError(_) => None
}
)
|. Belt.Option.flatMap(nullable(innerDecoder) >> Js.Null.toOption)
); |
@cknitt Indeed, that does seem pretty inconsistent. |
I've now made a "next" branch which has the proposed In OCaml: obj (fun {field} -> {
static = field.required "static" string;
dynamics = field.required "dynamics" (dict int)
}) and Reason: obj (({field}) => {
static: field.required("static", string),
dynamics: field.required("dynamics", dict(int)),
}) My main concern now is that since the getters aren't proper decoders, you can't compose them using combinators such as license : json |> optional(either(
at(["license", "type"], string),
field("license", string))), now becomes license : at.optional(["license", "type"], string)
|> Option.or_(field.optional("type", string)), which is confusingly different (and significantly less readable too). Other than that I'm still unsure about the ergonomics, and would love some feedback from real world usage and beginners trying to grok it. |
After reading the thread, it seems I can read
when I parse, it works if
but, that gave type error
@ncthbrt what is thanks. |
@bsr203 What you likely want is this:
If you really want it to be
This thread is about accomplishing the opposite, to have it return
|
I like this solution, I was having the same issue today. I have a field that's optional, but if that field is present, I don't want decode errors to swallowed up. |
This is my solution let optionalField = (fieldName, decoder, json) => {
let field = Util.Json.toDict(json)->Js.Dict.get(fieldName);
switch (field) {
| None => None
| Some(field) => Some(decoder(field))
};
};
let optionalWithDefault = (fieldName, decoder, json, default) => {
fieldName
->optionalField(decoder, json)
->Belt.Option.getWithDefault(default);
}; module Json = {
external toDict: Js.Json.t => Js.Dict.t('a) = "%identity";
} So in this case with object of {
"first_name": "Praveen",
"last_name": "Perera"
} Where the field let decode = (json) =>
Json.Decode.{
firstName: field("first_name", string, json),
lastName: optionalField("last_name", string, json)
} In this case if {
"first_name": "Praveen",
"last_name": null
} If I didn't want it to fail my decoder would look like this: let decode = (json) =>
Json.Decode.{
firstName: field("first_name", string, json),
lastName: optionalField("last_name", optional(string), json)
} |
There are some downsides to it. Since these decoders aren't ordinary decoders - they're essentially partially applied with the json argument - they don't compose well with other decoders. This is usually not a big deal, but there are some cases where this is a bit of a sore thumb. I've tried the next branch out a bit in a few of my own projects, but it really needs some more people to test and give feedback on it.
This is very unsafe. If the json is not an object, this might cause a crash or propagate garbage data. Also, since you don't have any type annotations on the functions that use this, you can pass any 1-ary function off as a decoder. You might want to consider using Js.typeof json = "object" &&
not (Js.Array.isArray json) &&
not ((Obj.magic json : 'a Js.null) == Js.null) Otherwise your solution looks good! |
HI @glennsl thanks a lot for the feedback, I've changed my code to: module Json = {
module Private = {
external toDict: Js.Json.t => Js.Dict.t(Js.Json.t) = "%identity";
let isJsonObject = json =>
Js.typeof(json) == "object"
&& !Js.Array.isArray(json)
&& !((Obj.magic(json): Js.null('a)) === Js.null);
};
let toDict = (json: Js.Json.t): Js.Dict.t(Js.Json.t) => {
Private.isJsonObject(json)
? Private.toDict(json) : Js.Dict.empty();
};
}; Let me know if you see any problems with this. I didn't see how I could use the
I think I prefer this solution because its just adding a new decoder and not introducing a breaking change. Thoughts? Should I do a PR? |
The My line of thinking here has evolved to the better approach being to move this logic outside the object decoder. I think that might make ti simpler, if a bit more boilerplatey. For example, instead of
maybe we should do
The latter has significantly more code, but also offers significantly more flexibility an is much easier to maintain over time I think, when more optional fields are added for different shapes. Maybe it should be modeled as a variant instead, to avoid invalid representations. That's much simpler to do with this. But then maybe other things are much harder. Again, I think we need more experience to figure out these idioms, and would love to get some more feedback on the next branch. I'm not doing anything with BuckleScript at all these days, so I'm not gaining any experience with it on my own. |
When parsing user input it's important to be able to give good error messages. For example in a http PATCH request, the
date_of_birth
field might only accept a UTC conformant string when the field is present. If a user passes in a number, we need to return with a400 BAD REQUEST
.When using the
optional
decoder in combination with the field decoder, if the field exists but the inner decoder fails, the final result will beNone
.In the previous example
{ dateOfBirth: optional(field("date_of_birth", utcString)) }
when fed with the payload{ "date_of_birth": 1234 }
would simply be{ dateOfBirth: None }
. The actual semantics that needed to be captured in this example was:{ }
becomes{ dateOfBirth: None }
{ "date_of_birth": "Wed, 14 Feb 2018 10:22:19 GMT" }
becomes{ dateOfBirth: Some("Wed, 14 Feb 2018 10:22:19 GMT")
{ "date_of_birth": 123457 }
becomesJson.Decoder.DecoderError("Expected a UTC string, got a number")
An
optionalField
decoder (or a decoder with a similar name) would solve this problem by returningNone
if the field isundefined
, else would bubble up the error from inner decoder.The text was updated successfully, but these errors were encountered: