Thoth.Json icon indicating copy to clipboard operation
Thoth.Json copied to clipboard

[FR] Decode.uri and Encode.uri

Open laurentpayot opened this issue 9 months ago • 9 comments

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?

laurentpayot avatar Mar 24 '25 10:03 laurentpayot

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 avatar Mar 24 '25 11:03 njlr

@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 😉)

laurentpayot avatar Mar 24 '25 11:03 laurentpayot

I thought exceptions were handled by Decode.map and would result to a Decode.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

MangelMaxime avatar Mar 24 '25 13:03 MangelMaxime

I am fine with adding supports for uri in 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).

laurentpayot avatar Mar 25 '25 16:03 laurentpayot

I am fine with adding supports for uri in 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).

This might have unwanted effects inside of a Decode.object, which uses exceptions as a short-cut mechanism

njlr avatar Mar 25 '25 16:03 njlr

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 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

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.

MangelMaxime avatar Mar 25 '25 17:03 MangelMaxime

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.

laurentpayot avatar Mar 25 '25 17:03 laurentpayot

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\""

MangelMaxime avatar Mar 25 '25 18:03 MangelMaxime

Sure, I would happily not use safeMap if Decode.uri itself is safe.

laurentpayot avatar Mar 25 '25 20:03 laurentpayot