Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FSharp deserialization of records with optional record fields #913

Open
tymokvo opened this issue Feb 26, 2024 · 2 comments
Open

FSharp deserialization of records with optional record fields #913

tymokvo opened this issue Feb 26, 2024 · 2 comments

Comments

@tymokvo
Copy link
Contributor

tymokvo commented Feb 26, 2024

I'm trying to figure out the canonical way to deserialize a record with an optional field that is not a scalar value.

Given the script:

[<CLIMutable>]
type SubThing = { value: string }

[<CLIMutable>]
type Thing = { subThing: SubThing option }

open YamlDotNet.Serialization

let d = Deserializer()

"""
subThing:
  value: "a"
"""
|> d.Deserialize<Thing>
|> printfn "%A"

Running this fails with the cryptic error:

(Line: 3, Col: 3, Idx: 13) - (Line: 3, Col: 3, Idx: 13): Exception during deserialization
Stopped due to error

I would expect this to result in { subThing = Some { value = "a" } }.

A simple omission of the field succeeds in the way I would expect:

"""
{}
"""
|> d.Deserialize<Thing>
|> printfn "%A"
{ subThing = None }

However, if the type of the subThing field is a scalar (e.g. string), the behavior is as I expect; omitting it results in None and supplying it results in Some <value>.

Is this behavior configurable in some way?

@EdwardCooke
Copy link
Collaborator

The Deserializer isn't meant to be instantiated directly. You should use the DeserializerBuilder so you can set all the options for it. It also adds all of the necessary objects and other required classes so it will work correctly.

I'm not an F# developer and don't know what the implications of removing option from the definition will do with other code.

Using the DeserializerBuilder and a couple of options and removing option from the subThing property made your code work:

[<CLIMutable>]
type SubThing = { value: string }

[<CLIMutable>]
type Thing = { subThing: SubThing }

open YamlDotNet.Serialization

let d = DeserializerBuilder().EnablePrivateConstructors().IncludeNonPublicProperties().Build()

"""
subThing:
  value: "a"
"""
|> d.Deserialize<Thing>
|> printfn "%A"

Results:

{ subThing = { value = "a" } }

@tymokvo
Copy link
Contributor Author

tymokvo commented Mar 1, 2024

I'm not an F# developer and don't know what the implications of removing option from the definition will do with other code.

It's basically equivalent to Nullable<T> in C# land. So, removing it makes the field required.

I don't know enough about how YamlDotNet works to know what's going on, but serializing the values of F# types also results in somewhat unexpected behavior which might hint at what's happening? It may be related to the implementation of Option.

Using this script, for example, the second value doesn't successfully round-trip to and from YAML.

#r "nuget: YamlDotNet"

[<CLIMutable>]
type SubThing = { value: string }

[<CLIMutable>]
type Thing = { subThing: SubThing option }

open YamlDotNet.Serialization

let d = DeserializerBuilder().EnablePrivateConstructors().IncludeNonPublicProperties().Build()

let s = SerializerBuilder().Build()

{
    subThing = None
}
|> s.Serialize
|> (fun v ->
    printfn "serialize None:\n%A" v
    v
)
|> d.Deserialize<Thing>
|> printfn "deserialize None:\n%A"

{
    subThing = Some { value = "a" }
}
|> s.Serialize
|> (fun v ->
    printfn "serialize Some:\n%A" v
    v
)
|> d.Deserialize<Thing>
|> printfn "deserialize Some:\n%A"
serialize None:
"subThing@: 
subThing: 
"
deserialize None:
{ subThing = None }
serialize Some:
"subThing@: &o0
  Value:
    value@: a
    value: a
subThing: *o0
"
(Line: 2, Col: 3, Idx: 17) - (Line: 2, Col: 8, Idx: 22): Property 'Value' not found on type 'Microsoft.FSharp.Core.FSharpOption`1[[FSI_0002+SubThing, FSI-ASSEMBLY2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]'.
Stopped due to error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants