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

Serialize Tuple as an object #116

Open
isaacabraham opened this issue Jun 14, 2022 · 4 comments
Open

Serialize Tuple as an object #116

isaacabraham opened this issue Jun 14, 2022 · 4 comments

Comments

@isaacabraham
Copy link

It would be nice to be able to serialize a tuple directly to an object rather than a list e.g. ("name", "foo") => { "name" : "foo" }. I can't see if there's any way to do that. My use case is that I want to serialize a list of tuples to JSON in the above style rather than as a list of lists. I can use a Map / Dictionary here, except that that enforces uniqueness of the key, which I don't want.

Any ideas?

@Tarmil
Copy link
Owner

Tarmil commented Jun 16, 2022

To be clear, you would like something like this?

JsonSerializer.Serialize [("name", "foo"); ("other", "bar")]
// --> [{"name":"foo"},{"other":"bar"}]

I think that's a bit too specific and niche to include in FSharp.STJ. But you can absolutely implement a dedicated Converter that handles (string * _) list this way.

open System.Text.Json
open System.Text.Json.Serialization
open FSharp.Core.CompilerServices

type LookupListConverter<'V>() =
    inherit JsonConverter<(string * 'V) list>()

    override this.Read(reader, typeToConvert, options) =
        if reader.TokenType <> JsonTokenType.StartArray then raise (JsonException "Expected array")
        let mutable l = ListCollector()
        while reader.Read() && reader.TokenType <> JsonTokenType.EndArray do
            if reader.TokenType <> JsonTokenType.StartObject then raise (JsonException "Expected object")
            if not (reader.Read() && reader.TokenType = JsonTokenType.PropertyName) then raise (JsonException "Expected non-empty object")
            let key = reader.GetString()
            let value = JsonSerializer.Deserialize<'V>(&reader, options)
            if not (reader.Read() && reader.TokenType = JsonTokenType.EndObject) then raise (JsonException "Expected single-property object")
            l.Add((key, value))
        l.Close()

    override this.Write(writer, value, options) =
        writer.WriteStartArray()
        for k, v in value do
            writer.WriteStartObject()
            writer.WritePropertyName(k)
            JsonSerializer.Serialize<'V>(writer, v, options)
            writer.WriteEndObject()
        writer.WriteEndArray()

type LookupListConverter() =
    inherit JsonConverterFactory()

    override this.CanConvert(typeToConvert) =
        typeToConvert.IsGenericType &&
        typeToConvert.GetGenericTypeDefinition() = typedefof<_ list> &&
        let tparam = typeToConvert.GetGenericArguments()[0]
        tparam.IsGenericType &&
        tparam.GetGenericTypeDefinition() = typedefof<_ * _> &&
        tparam.GetGenericArguments()[0] = typeof<string>

    override this.CreateConverter(typeToConvert, options) =
        let tparam = typeToConvert.GetGenericArguments().[0].GetGenericArguments().[1]
        typedefof<LookupListConverter<_>>.MakeGenericType(tparam)
            .GetConstructor([||])
            .Invoke([||])
        :?> JsonConverter

// Usage:
let options = JsonSerializerOptions()
options.Converters.Add(ListLookupConverter())
JsonSerializer.Serialize([("name", "foo"); ("other", "bar")], options)

@abelbraaksma
Copy link

Quite often, two-tuples are used in (unique or not) KV setting, similar to how the dict constructor works. Perhaps we could consider a setting like TwoTupleAsKeyValueObjects.

I usually work around this limitation by using a record instead of a tuple, but it’d be nice if this were configurable. I do get the “it’s too niche” comment though, but is it really? ;).

@Tarmil
Copy link
Owner

Tarmil commented Jan 9, 2023

I do get the “it’s too niche” comment though, but is it really? ;).

Well, I was basing it on the fact that I don't think I've ever seen this kind of JSON in the wild, but apparently it's common enough that multiple people are requesting it 😄

We could add an option for this. What should it apply to exactly though? Any 'K * 'V tuple? Specifically string * 'V? Only lists of these? My guess would be to apply it specifically to (string * 'V) list, like in my code above, but I'm open to suggestions (and MRs).

@abelbraaksma
Copy link

abelbraaksma commented Jan 12, 2023

@Tarmil Any enumerable seems reasonable (not just list, I mean), and yes, string * 'T seems sensible enough. I can imagine it being useful to other types than string, but it’s probably better to be strict.

It’ll be behind an option, so if we want different behaviour, like people wanting this for all tuples, we can enable that explicitly later.

@isaacabraham you wrote the OP, what do you think?

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

3 participants