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

Proposal: Encode syntax

Open njlr opened this issue 2 months ago • 3 comments

I think we can make writing JSON programaticaly a little more convenient without touching the core abstractions.

Before:

Encode.object
  [
    "foo",
      Encode.object
        [
          "bar", Encode.list [ Encode.int 123; Encode.string "abc" ]
        ]
  ]

After:

json {
  "foo" <~ 
    json { 
      "bar" <~ json { 
        123
        "abc" 
      }
    }
}
{
  "foo": {
    "bar": [
      123,
      "abc"
    ]
  }
}

Happy to riff on the exact syntax.

And the implementation (could not doubt be more efficient):

#r "nuget: Thoth.Json.Core"

open Thoth.Json.Core

type JsonValueHelper =
  static member inline ($) (_ : JsonValueHelper, value : IEncodable) =
    value

  static member inline ($) (_ : JsonValueHelper, value : string) =
    Encode.string value

  static member inline ($) (_ : JsonValueHelper, value : int) =
    Encode.int value

  static member inline ($) (_ : JsonValueHelper, value : int64) =
    Encode.int64 value

  static member inline ($) (_ : JsonValueHelper, value : bool) =
    Encode.bool value

  // etc...

let inline toJsonValue value : IEncodable =
  Unchecked.defaultof<JsonValueHelper> $ value

type JsonProperty = string * IEncodable

type JsonBuilder() =
  membe this.Yield(prop : JsonProperty) =
    Seq.singleton prop

  member inline this.Yield(value : _) =
    Seq.singleton (toJsonValue value)

  member this.YieldFrom(props : JsonProperty seq) =
    props

  member this.YieldFrom(xs : IEncodable seq) =
    xs

  member this.Combine(xs : JsonProperty seq, ys) =
    Seq.append xs ys

  member this.Combine(xs : IEncodable seq, ys) =
    Seq.append xs ys

  member this.Zero() =
    Seq.empty

  member this.Delay(f) =
    f ()

  member this.For(seq : seq<'a>, mapFunction : 'a -> seq<'b>) =
    seq
    |> Seq.collect mapFunction

  member this.Run(props : JsonProperty seq) : IEncodable =
    props
    |> Seq.toList
    |> Encode.object

  member this.Run(value : IEncodable) : IEncodable =
    value

  member this.Run(elements : IEncodable seq) : IEncodable =
    elements
    |> Seq.toList
    |> Encode.list

let json = JsonBuilder()

let inline (<~) (name : string) (value : _) : JsonProperty =
  name, toJsonValue value

njlr avatar Oct 09 '25 19:10 njlr

This seems interesting, we could expose it either via a new package or via a module in Thoth.Core.

I think the module version would be good enough so people can "opt-in" on the feature if they like it.

I have remarks coming to my mind when seeing this notation:

  1. <~ is not always easy on some keyboard layout
  2. I can't seem to be able to find a Fantomas settings allowing to keep the code formatted as
json {
  "foo" <~ 
    json { 
      "bar" <~ json { 
        123
        "abc" 
      }
    }
}

it seems to always format it as

json {
    "foo"
    <~ json {
        "bar"
        <~ json {
            123
            "abc"
        }
    }
}

MangelMaxime avatar Oct 10 '25 08:10 MangelMaxime

Love it! How does <-- work with fantomas?

goswinr avatar Oct 10 '25 08:10 goswinr

All the infix gives the same formatting except .. but this one an invalid syntax 😅

MangelMaxime avatar Oct 10 '25 09:10 MangelMaxime