[FR] Decode.uri and Encode.uri
Hi, I could not find Decode.uri (or equivalent) that would be the same as Decode.string |> Decode.map System.Uri.
Decoding URIs is very common so I had to write the following extension to add the very handy Decode.uri and Encode.uri features:
[<AutoOpen>]
module ThothExtensions
open System
open Thoth.Json.Core
module Thoth =
module Json =
module Core =
module Decode =
let uri: Decoder<Uri> = Decode.string |> Decode.map Uri
module Encode =
let uri: Encoder<Uri> = _.ToString() >> Encode.string
Just like the super-useful Decode.guid and Encode.guid, would it be possible to have Decode.uri and Encode.uri directly out of the box of Thoth.Json?
Beware of exceptions in the Uri constructor!
#r "nuget: Thoth.Json.Core, 0.7.0"
#r "nuget: Thoth.Json.Newtonsoft"
open System
open Thoth.Json.Core
module Thoth =
module Json =
module Core =
module Decode =
let private tryParseUri (x : string) : Uri option =
try
Uri x |> Some
with :?UriFormatException ->
None
let uri : Decoder<Uri> =
Decode.string
|> Decode.andThen
(fun (x : string) ->
match tryParseUri x with
| Some uri ->
Decode.succeed uri
| None ->
Decode.fail "Invalid URI")
open Thoth.Json.Core
open Thoth.Json.Newtonsoft
let cases =
[
"\"https://example.org\""
"\"inv^^1alid\""
]
for case in cases do
printfn "%s" case
case
|> Decode.fromString Decode.uri
|> printfn "%A"
printfn ""
@njlr Thank you for pointing this out! I thought exceptions were handled by Decode.map and would result to a Decode.fail. @MangelMaxime any reason for this? (before I try to write a wrapper for Decode.map 😉)
I thought exceptions were handled by
Decode.mapand would result to aDecode.fail.
I was starting to make a complex explanations but there is a simple one. 😅
Decode.map is not always called, for example you can do Decode.fromString Decode.uri myJson
The other reason, why Decode.fromString don't capture any exceptions happenings in the decoder chain is because some people wanted to be able to emit their own exception and capture it in their application.
So the idea, is that Thoth.Json will capture all the exception related to decoder/encoder but arbitrary exceptions are not captured and left to the user to handle if needed. But in general, you never have to worry about it unless you create your own low-level encoders/decoders like you are doing.
I am fine with adding supports for uri in Thoth.Json standard API
I am fine with adding supports for
uriin Thoth.Json standard API
@MangelMaxime that would make my day 👍 (if it is safe)
I ended up adding safeMap to my extensions above:
let safeMap (f: 'a -> 'b) (decoder: Decoder<'a>) : Decoder<'b> =
try
Decode.map f decoder
with ex ->
Decode.fail ex.Message
Do you think also adding this tiny wrapper to the standard API would be useful? It was quite a surprise to realize Decode.map was actually unsafe (I’m only writing tests when module features are stabilized).
I am fine with adding supports for
uriin Thoth.Json standard API@MangelMaxime that would make my day 👍 (if it is safe)
I ended up adding
safeMapto my extensions above:let safeMap (f: 'a -> 'b) (decoder: Decoder<'a>) : Decoder<'b> = try Decode.map f decoder with ex -> Decode.fail ex.Message
Do you think also adding this tiny wrapper to the standard API would be useful? It was quite a surprise to realize
Decode.mapwas actually unsafe (I’m only writing tests when module features are stabilized).
This might have unwanted effects inside of a Decode.object, which uses exceptions as a short-cut mechanism
This might have unwanted effects inside of a
Decode.object, which uses exceptions as a short-cut mechanism
Do we ? I checked the code and I don't see where we are doing that.
There are places with Unchecked.defaultOf<_> which could throw perhaps I don't remember how we use them.
I ended up adding
safeMapto my extensions above:let safeMap (f: 'a -> 'b) (decoder: Decoder<'a>) : Decoder<'b> = try Decode.map f decoder with ex -> Decode.fail ex.Message
I don't think this is a good idea, the error should be handle at the type encoder level not globally. Using safeMap could cause unwanted effect if the user want to propagate an exception etc.
I believe the current behavior is good because we know what to expect.
But as you discovered, the benefit of Thoth.Json is that the API is extensible so if people want to create their own decoders/encoders they can do it.
It happens in the past for people who wanted to use CEs instead of the standard API for example, etc.
This might have unwanted effects inside of a Decode.object, which uses exceptions as a short-cut mechanism
I mean leaving map as it is, simply adding safeMap.
I mean leaving ‘map‘ as it is, simply adding ’safeMap‘.
That's what I understood, but I consider this is not a good idea to use this variant. See my answer above, for these reasons I don't feel like adding it to the standard library is a good, and would recommend to not use it in your code too.
IHMO, this is best to handle error handling at the type decoder level like done for all the other types.
Edit:
Remember that the user can call Decode.uri directly without passing any other decoders and they should still have the error reported.
Decode.fromString Decode.uri "\"my-invalid-url\""
Sure, I would happily not use safeMap if Decode.uri itself is safe.