Fable: ofJson/toJson with custome key attributes

Created on 15 Sep 2017  路  15Comments  路  Source: fable-compiler/Fable

Is there a way to make ofJson/toJson serialize/deserialize Records with custom key name?
e.g.

```f#
type Card = {
[ Title: string
[ Content: string
}

let cardJson = toJson({Title = "aaa"; Content = "bbb"})
// cardJson = "{\"title\":\ "aaa\",\ "content\": \"bbb\"}"
```

Most helpful comment

After I was more familiar with FSharp & Fable's ofJson/toJson implementation, I found a more particular way to custom type's serialization, so I put the code here for anyone are looking for the same solution.

I see the deflate function implement ofJSON static method, and toJSON is a standard method for custom JSON serialization in JS, so we can simply using this feature since F# is a FP language above an OO platform (dotnet) 馃槃

type People = {
  Name: string
  Age: int
} with
  member x.toJSON () = 
    createObj [
      "name" ==> x.Name
      "age" ==> x.Age
    ]
  static member ofJSON (json) = 
    {
      Name = !!json?name
      Age = !!json?age
    }


let people = {Name = "Mick"; Age = 23 }
let pToJson = toJson(people) 
printfn "%A" pToJson
let pOfJson = ofJson<People>(pToJson)
printfn "%A" pOfJson


type Au = 
  | A of string
  | B of int
 with
  member x.toJSON () = 
    let (tag, data) = match x with
    | A data -> ("a", data :> obj)
    | B data -> ("b", data :> obj)
    createObj [
      "type" ==> tag
      "data" ==> data
    ]
  static member ofJSON (json) = 
    let lowerTag: string = !!json?``type``
    match lowerTag with
    | "a" -> A (!!json?data)
    | "b" -> B (!!json?data)

let auToJson = toJson(B 2) 
printfn "%A" auToJson

ofJson<Au>(auToJson) |> printfn "%A"

repl link

This could be a little verbose when there are many fields, but we can using some FFI sugar and reflection to solve this partially:

```F#
open Fable.Core
open Fable.Core.JsInterop
open System
open System.Text.RegularExpressions
open Fable.Import

[ let delete<'T> obj key = jsNative

type People = {
Name: string
Age: int
} with
member x.toJSON () =
let copyed = JS.Object.assign (createObj [], jsThis :> obj)
copyed?name <- copyed?Name
delete copyed "Name"
copyed
static member ofJSON (json) =
{ Name = !!json?name; Age = !!json?Age } // don't know how to initialize a Record instance with out inflate (inflate would call ofJSON to cause a loop)

let people = {Name = "Mick"; Age = 23 }
let pToJson = toJson(people)
printfn "%A" pToJson
let pOfJson = ofJson(pToJson)
printfn "%A" pOfJson

```

I still think using attributes is the most elegant approach. But this also give a hint to implement such a plugin, although recently I'm going to using SAFE-Stack in a side project and being busy with some more high level work...

All 15 comments

Not at the moment, sorry. You can use CompiledName but this will also affect the JS code.

@alfonsogarciacaro I see CompiledName in docs, looks it only works for Union Types.

Ah, true. I didn't remember that 馃槄 Another solution could be to use properties, though it's admittedly more verbose:

type Card =
  private { title: string; content: string }
  member this.Title = this.title
  member this.Content = this.content
  static member Create(t, c) = { title = t; content = c }

let c = Card.Create("foo", "bar")

Fable.Core.JsInterop.toJson c |> printfn "%A"

Personally when I need lower case for JS interop I just use lower case in the F# record, but I know this is a matter of coding style. At the beginning of Fable, I tried to make automatic case conversion between F# and JS types but this caused too many conflicts :/

@alfonsogarciacaro Do you know the rust's JSON library serde-rs? It made a flexible way to custom the serializing/deserializing by attributes, although this might be more complicated in F# since there are records/union types/classes/interfaces/abstract classes...

No, sorry. We just added the toJson/ofJson helpers to Fable.Core to make it easier to share and exchange F# types between back and front ends but more fine-grained JSON serialization is not in the roadmap at the moment (at least not in Fable.Core but PRs are always welcome!).

There are some helpers for custom JSON parsing in Fable.PowerPack but I haven't used them myself yet.

I see, thanks for your linking. I also notice fable doesn't support custome decorators/attributes for now, would this be in the roadmap? A more fine-grained JSON library like serde-rs could be implemented as a third-party library with custom attributes.

BTW, what about passing a custom function to ofJSON/toJSON like JSON.parse/stringify in JS as a more light weight approach ? would this be possible and I'd love to give a try

@zaaack In the past I used the JSON.parse/stringify by adding a custom function to handle special case. So this is a possibilities, in the end you are in JS when running your code :)

@zaaack If this feature is to be implemented, please keep in mind that using custom attributes will make sharing the models between server and client harder. An attribute such as [<JsonRename>] or something else for that matter will probably live within Fable.Core or Fable.Core.JsInterop and you would need #IF FABLE compiler directives whenever you want to use the attributes.

Thanks for advices of all of you! I'll make more research recently, hoping it would not be too hard!馃槄

BTW, what about passing a custom function to ofJSON/toJSON like JSON.parse/stringify in JS as a more light weight approach ?

This is actually what toJson/ofJson do, but it's not trivial as you need to know how Fable stores metadata info for that, which is not documented yet (partly because we didn't want people to start relying on it in case we had to change it in future versions).

I also notice fable doesn't support custome decorators/attributes for now, would this be in the roadmap?

Compiling attributes to JS is a bit tricky (see #347 and #981), but attributes are visible to plugins so it could be possible to write a plugin that interprets them at compile time. Unfortunately, documentation for plugin has also been temporarily removed as not many devs were using them so I'm thinking on improving the API (especially for Fable AST generation).

Thanks for your explaination! So I'll keep on watching this and waiting for the plugin API being stable, really excited about this.

If you're only interested in lowering the case of the fields, you can also do it easily without the trouble of custom parameters ;)

open Fable.Import
open Fable.Core
open Fable.Core.JsInterop

type Card =
  { TitleFoo: string
    Content: string }

let toLowerJson (x: 'T) =
  let deflated = deflate x
  let lower = obj()
  for k in JS.Object.keys(deflated) do
    let loweredKey = (string k.[0]).ToLower() + k.[1..]
    lower?(loweredKey) <- deflated?(k)
  JS.JSON.stringify(lower)

{ TitleFoo = "Foo"; Content = "Bar" }
|> toLowerJson
|> printfn "%s"

@alfonsogarciacaro Thanks锛乀his helps a lot!

After I was more familiar with FSharp & Fable's ofJson/toJson implementation, I found a more particular way to custom type's serialization, so I put the code here for anyone are looking for the same solution.

I see the deflate function implement ofJSON static method, and toJSON is a standard method for custom JSON serialization in JS, so we can simply using this feature since F# is a FP language above an OO platform (dotnet) 馃槃

type People = {
  Name: string
  Age: int
} with
  member x.toJSON () = 
    createObj [
      "name" ==> x.Name
      "age" ==> x.Age
    ]
  static member ofJSON (json) = 
    {
      Name = !!json?name
      Age = !!json?age
    }


let people = {Name = "Mick"; Age = 23 }
let pToJson = toJson(people) 
printfn "%A" pToJson
let pOfJson = ofJson<People>(pToJson)
printfn "%A" pOfJson


type Au = 
  | A of string
  | B of int
 with
  member x.toJSON () = 
    let (tag, data) = match x with
    | A data -> ("a", data :> obj)
    | B data -> ("b", data :> obj)
    createObj [
      "type" ==> tag
      "data" ==> data
    ]
  static member ofJSON (json) = 
    let lowerTag: string = !!json?``type``
    match lowerTag with
    | "a" -> A (!!json?data)
    | "b" -> B (!!json?data)

let auToJson = toJson(B 2) 
printfn "%A" auToJson

ofJson<Au>(auToJson) |> printfn "%A"

repl link

This could be a little verbose when there are many fields, but we can using some FFI sugar and reflection to solve this partially:

```F#
open Fable.Core
open Fable.Core.JsInterop
open System
open System.Text.RegularExpressions
open Fable.Import

[ let delete<'T> obj key = jsNative

type People = {
Name: string
Age: int
} with
member x.toJSON () =
let copyed = JS.Object.assign (createObj [], jsThis :> obj)
copyed?name <- copyed?Name
delete copyed "Name"
copyed
static member ofJSON (json) =
{ Name = !!json?name; Age = !!json?Age } // don't know how to initialize a Record instance with out inflate (inflate would call ofJSON to cause a loop)

let people = {Name = "Mick"; Age = 23 }
let pToJson = toJson(people)
printfn "%A" pToJson
let pOfJson = ofJson(pToJson)
printfn "%A" pOfJson

```

I still think using attributes is the most elegant approach. But this also give a hint to implement such a plugin, although recently I'm going to using SAFE-Stack in a side project and being busy with some more high level work...

Closing, as now we have Thot for more fine-grained Json serialization 馃憤

Was this page helpful?
0 / 5 - 0 ratings

Related issues

forki picture forki  路  3Comments

alfonsogarciacaro picture alfonsogarciacaro  路  3Comments

MangelMaxime picture MangelMaxime  路  3Comments

nozzlegear picture nozzlegear  路  3Comments

tomcl picture tomcl  路  4Comments