Tarmil / FSharp.SystemTextJson

System.Text.Json extensions for F# types
MIT License
323 stars 44 forks source link

Types with mutual recursion are not supported #155

Open jmfallecker opened 1 year ago

jmfallecker commented 1 year ago

Discriminated unions that are self-referential and any mutually recursive types are not supported. Using the $id and $ref keys in JSON should allow this type of structure to be serialized.

for a type definition like such:

type A = { B of B } and B = { A of A }

the json should end up being something like:

{ "A": { "$id": "1", "B": { "$ref": "2" } }, "B": { "$id": "2", "A": { "$ref": "1" } } }

This is an incredibly simplified example, but there's an approach we can take using reflection to figure out what properties cause a cycle in a generic data structure. Once we've identified the cycle, we can use comparison of all other fields (excluding the cyclical part) to understand which records reference which other records.

I've included a first draft of a function to determine if a type is recursive or not.

isRecursive.txt

Tarmil commented 1 year ago

I would be curious to see your actual use case in more detail. Mutually recursive types work fine, for example:

type A =
  { b: B option }
and B =
  { a: A }

let x = { b = Some { a = { b = None } } }

JsonSerializer.Serialize(x, JsonFSharpOptions().ToJsonSerializerOptions())
// --> {"b":{"a":{"b":null}}}

However, self-recursive values are indeed not supported. System.Text.Json has ReferenceHandler.Preserve that allows producing the kind of JSON you list, but its documentation indicates:

This feature can't be used to preserve value types or immutable types. On deserialization, the instance of an immutable type is created after the entire payload is read. So it would be impossible to deserialize the same instance if a reference to it appears within the JSON payload.

The same applies to the way FSharp.SystemTextJson deserializes records and unions: the returned value is created after reading the payload, so it can't be referenced from inside the payload.

jmfallecker commented 8 months ago

My main use case was within a bolero application. I was attempting to serialize a model to be able to pass it to an API, then deserialize the same model on the server side.

It works fine for non recursive types, like you said.

Sometimes when domain modeling, a recursive type just makes things clearer, so I'd hate to have to avoid them.

I am responding quite a bit later than when I originally posted this, but I'll check out the code again and see what you're talking about with the order of events.