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

JSON: Optionally write union variant tags and names #4293

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

karl-zylinski
Copy link
Contributor

@karl-zylinski karl-zylinski commented Sep 22, 2024

Introduces an option to make JSON marshaller write out union variant name and tag. These names and tags are automatically looked for when unmarshalling happens, in a backwards compatible way.

For a union looking like this

A :: struct {
	a: int,
}

B :: struct {
	b: string,
}

U :: union {
	rawptr,
	A,
	B,
}

u: U = B { b = "Hellope" }

It would write this:

{
	"$data": {
		"b": "Hellope",
	},
	"$name": "B",
	"$tag": 2
}

When unmarshalling into a union, it will look for these $name and $tag fields etc. If found it will use them and try to find a matching union variant.

It writes both name and tag so that it can first try with name, because that is makes it possible to move things in the union. However, if you renamed a type, then the name wont match and it will instead try with the tag.

Without this the above would unmarshal into a union of variant A instead, which is not what the user expects.

Test program that shows how it works and how to enable the marshalling option:

package main

import "core:fmt"
import "core:encoding/json"
import "core:reflect"

A :: struct {
	a: int,
}

B :: struct {
	b: string,
}

U :: union {
	rawptr,
	A,
	B,
}

main :: proc() {
	um: U = B {
		b = "Hellope",
	}

	bytes, marshal_err := json.marshal(um, opt = {
		write_union_variant_info = true,
		pretty = true,
	})

	if marshal_err != nil {
		fmt.println(marshal_err)
	}

	fmt.println("Marshalled string:")
	fmt.println(string(bytes))

	u: U

	error := json.unmarshal(bytes, &u)

	if error != nil {
		fmt.println(error)
	}

	fmt.println("\nUnmarshalled into:")
	fmt.println(u)
}

when run it prints this:

Marshalled string:
{
	"$data": {
		"b": "Hellope"
	},
	"$name": "B",
	"$tag": 2
}

Unmarshalled into:
B{b = "Hellope"}

The above is similar to the repro in this issue: #3474, but with the desired outcome.

Things to consider:

  • Is a best-fit search also wanted? Or perhaps even what we should try to implement before any of this? My motivation for trying this is that I don't trust a JSON marshaller / unmarshaller unless it has this kind of variant info written down.
  • Currently it can only write $name for variants that have type info Type_Info_Named. For basic types like f32 it would only write the $tag. I can use reflect.write_typeid in the marshaller to write the "correct" type for something like an f32, but no good way to compare against it in the unmarshaller without doing something similar there, which starts feeling a bit hacky.

…name and tag. These names and tags are automatically looked for when unmarshalling happens, in a backwards compatible way.
@flysand7
Copy link
Contributor

Is a best-fit search also wanted? Or perhaps even what we should try to implement before any of this? My motivation for trying this is that I don't trust a JSON marshaller / unmarshaller unless it has this kind of variant info written down.

This can get into a grey area of weirdnesses, that if we allow it, that would introduce a whole world of complexity into the marshaller. Constructs like union {int, str} are definately useful, and should be unmarshalled correctly, even if variant field isn't specified in the JSON. But on the other hand union {f32, f64} shouldn't be allowed to get unmarshalled into.

So it's not a best-fit exactly, but I guess best unambiguous fit, and that's what I would expect the package to be able to do. The best thing here is to only allow unions that only contain specific base types:

  • nil (most unions contain it anyway, like the Maybe one)
  • floats
  • booleans
  • string
  • []T, where T is one of this list
  • map[string]T, where T is one of this list

Everything else can be disallowed, unless I'm missing something out.

With this, it's not too hard to make something that will "just work" and map well to some of the JSON files with non-uniform structure.

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

Successfully merging this pull request may close these issues.

2 participants