fjoppe / Legivel

F# Yaml 1.2 parser
https://fjoppe.github.io/Legivel
The Unlicense
60 stars 5 forks source link

Deserialize Discriminated Union with primitive scalar data type #13

Closed Brains closed 5 years ago

Brains commented 5 years ago

Description

Need to deserialize Discriminated Union with primitive int data. Don't want to use attributes YamlValue, YamlField to keep types clear for easy usage from F# Interactive

Repro steps

  1. Define types
    
    type Salary =
    | Hourly  of int
    | Monthly of int

type User = { name: string dept: string cost: Salary }

2. Try to deserialize it from different `.yml` files:
```fs
System.IO.File.ReadAllText "Datas\Users.yml" 
|> Serialization.Deserialize<User>
name: Albert Einstein
dept: Physics
cost: Hourly 65
---
name: Albert Einstein
dept: Physics
cost: Hourly (65)
---
name: Albert Einstein
dept: Physics
cost: {Hourly,65}
---

Expected behavior

Deserialize all data successfully

Actual behavior

Errors:

Message = "Union case 'Hourly 65' not availble in type: 'FSI_0075+Salary'"
Message = "Missing value for field: 'cost'"
Message = "Expecting a Scalar Node"
Message = "Document cannot be mapped"
Message = "Type mismatch 'tag:yaml.org,2002:map' for tag: Int32"

Known workarounds

Even with attributes YamlValue, YamlField can't manage it to work:

[<YamlField("TypeOf")>]
type Salary =
    | Hourly
    | Monthly of int

System.IO.File.ReadAllText "Datas\Users.yml" 
|> Serialization.Deserialize<Salary>
null: 60
TypeOf: Monthly
---
TypeOf: {Monthly,60}
Brains commented 5 years ago

Is correct deserialization without attributes possible to achieve with Legivel.Mapper customizations?

fjoppe commented 5 years ago

Interesting feature, never thought of this pattern.

The issue with Yaml to Discriminated Unions mapping is that you can identify various useful yaml-notation patterns, for which you need to process various nesting levels of Yaml Nodes. And support those, without excuding other features. Challenge :)

This pattern adds a new idea to what I had in mind (thanks!). The cost/Salary construct is the most interesting.

cost: Hourly 65
cost: Hourly (65)

Both are entries of a yaml mapping, key/value pairs. In both cases, a(ny) mapper would need to process the string, wich is contained in value part. That is not supported in the current version.

cost: {Hourly,65}

This could be changed to:

cost: {Hourly:65} # key/value pair

Which I expect it should be parseable with the current version.

The customizations in Legivel.Mapper are meant for you to add your own mapping methods, while reuse what's already there. I haven't tested the usibility much for external parties. I'm doing offline an raml parser, using Legivel, and discovered that some stuff lacked customization. So if you'd like to use the customization, please regard that this area still "hot" for change.

The Yaml attributes are currently required to hint the parser what to do where, and keeping doors open for other features. If you'd like to do it without these attributes, this would resort to writing your own mapper, or customize the current one, wich is quite some work. Another way to keep you FSI script clean is to create an intermediate type, to which the mapper deserializes, and to map this intermediate to a clean target type.

Thanks for your contribution.

Brains commented 5 years ago

Which I expect it should be parseable with the current version.

Unfortunately, no. Or I'm just doing something wrong.

Here is slightly modified sample from your tutorial:

type UnionCase =
    |   One of int
    |   [<YamlValue("two")>] Two of int

Deserialize<UnionCase> "{One:1}"
Deserialize<UnionCase> "{two:2}"

and result:

> Deserialize<UnionCase> "{One:1}";;
val it : DeserializeResult<UnionCase> list =
  [Error {Warn = [];
          Error = [{Location = (l0, c0);
                    Message = "Document cannot be mapped";}];
          StopLocation = (l1, c8);}]

> Deserialize<UnionCase> "{two:2}";;
val it : DeserializeResult<UnionCase> list =
  [Error {Warn = [];
          Error = [{Location = (l0, c0);
                    Message = "Document cannot be mapped";}];
          StopLocation = (l1, c8);}]

Another way to keep you FSI script clean is to create an intermediate type, to which the mapper deserializes, and to map this intermediate to a clean target type.

Agree, also thought about that. Also, intermediate types will contain

type Value = {value:int}

so I can parse them like in your examples. Intermediate types resolve all problems at once.

Thank you for your repo.

Brains commented 5 years ago

Confirmed. This works:

type Value = {value:int}

[<YamlField("sal")>]
type Salary =
    | Hourly of Value

Deserialize<Salary> "{value: 1, sal: Hourly}"

results in

> Deserialize<Salary> "{value: 1, sal: Hourly}";;
val it : DeserializeResult<Salary> list = [Succes {Data = Hourly {value = 1;};
                                                   Warn = [];}]
rbauduin commented 4 years ago

I have the type

type Entity = {
 id              : int
}

and yaml:

entities_014:
  id: 14

which I parse succesfully with Legivel.

Is it now possible to switch to these types

type EntityId = EntityId of int
type Entity = {
 id              : EntityId
}

and still parse the same YAML files? From my understanding it was not possible at the time this issue was discussed, but has it changed in the last year?

fjoppe commented 4 years ago

Unfortunately, I haven't worked on this part. I'm still working very hard on improving Legivel's performance. Which appears to be a much bigger adventure than I anticipated.