Skip to content

feature: Support JSON Tag 262? #657

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

Open
theory opened this issue Apr 18, 2025 · 3 comments
Open

feature: Support JSON Tag 262? #657

theory opened this issue Apr 18, 2025 · 3 comments

Comments

@theory
Copy link
Contributor

theory commented Apr 18, 2025

Is your feature request related to a problem? Please describe.

I inquired with the CBOR mail list about adding a tag for JSON numbers as string, toward having an official tag number to use for the pattern I posted in #441. They suggested that tag 262, an embedded JSON value, might also apply. However, since encoding/json values into any except for json.Number resolve to core Go types, and will vary, I can't see a clear way to register 262 with TagSet.Add.

Describe the solution you'd like

Either formal support for tag 262 or else some sort of way to register a tag that resolves to more than one type.

Describe alternatives you've considered

I tried to create a JSON type that wraps JSON values:

type JSON struct {
	any
}

func NewJSON(val any) JSON { return JSON{val} }

func (jt JSON) MarshalCBOR() ([]byte, error) {
	data, err := jt.MarshalJSON()
	if err != nil {
		return nil, err
	}
	return cbor.RawTag{Number: 262, Content: data}.MarshalCBOR()
}

func (jt *JSON) UnmarshalCBOR(b []byte) error {
	var tag cbor.RawTag
	if err := cbor.Unmarshal(b, &tag); err != nil {
		return err
	}
	return jt.UnmarshalJSON(tag.Content)
}

func (jt JSON) MarshalJSON() ([]byte, error) {
	return json.Marshal(jt.any)
}

func (jt *JSON) UnmarshalJSON(b []byte) error {
	enc := json.NewDecoder(bytes.NewReader(b))
	enc.UseNumber()
	return enc.Decode(&jt.any)
}

However, an attempt to marshal a value of this type fails; I probably don't fully understand the RawTag bit:

func main() {
	user := NewJSON(map[string]any{
		"name": "theory",
		"id":   json.Number("1024"),
	})

	data, err := cbor.Marshal(user)
	if err != nil {
		panic(err)
	}
}

Output:

panic: cbor: error calling MarshalCBOR for type main.JSON: unexpected EOF

Am I on the right track?

@fxamacker
Copy link
Owner

fxamacker commented Apr 20, 2025

Am I on the right track?

@theory yes the alternative approach you considered works if you use cbor.Tag instead of cbor.RawTag, like this:

https://go.dev/play/p/bjXlFdYTg_P

// Example program for:
// https://github.com/fxamacker/cbor/issues/657
package main

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/fxamacker/cbor/v2"
)

type JSON struct {
	any
}

func NewJSON(val any) JSON { return JSON{val} }

func (jt JSON) MarshalCBOR() ([]byte, error) {
	data, err := jt.MarshalJSON()
	if err != nil {
		return nil, err
	}
	// IMPORTANT: Need to use cbor.Tag (instead of cbor.RawTag) here.
	// Only use cbor.RawTag if tag.Content is already encoded in CBOR.
	tag := cbor.Tag{Number: 262, Content: data}
	return cbor.Marshal(tag)
}

func (jt *JSON) UnmarshalCBOR(b []byte) error {
	var tag cbor.Tag
	if err := cbor.Unmarshal(b, &tag); err != nil {
		return err
	}
	// Check if tag number is 262.
	if tag.Number != 262 {
		return fmt.Errorf("failed to unmarshal to JSON: got tag number %d, expect tag number %d", tag.Number, 262)
	}
	// Check if tag content is []byte.
	switch content := tag.Content.(type) {
	case []byte:
		return jt.UnmarshalJSON(content)
	default:
		return fmt.Errorf("failed to unmarshal to JSON: got tag content type %T, expect tag content []byte", tag.Content)
	}
}

func (jt JSON) MarshalJSON() ([]byte, error) {
	return json.Marshal(jt.any)
}

func (jt *JSON) UnmarshalJSON(b []byte) error {
	enc := json.NewDecoder(bytes.NewReader(b))
	enc.UseNumber()
	return enc.Decode(&jt.any)
}

func main() {
	user := NewJSON(map[string]any{
		"name": "theory",
		"id":   json.Number("1024"),
	})

	data, err := cbor.Marshal(user)
	if err != nil {
		panic(err)
	}

	edn, _ := cbor.Diagnose(data)

	fmt.Printf("encoded CBOR is %d bytes because this example includes JSON encoding embedded inside CBOR.\n", len(data))
	fmt.Printf("             Hex: %x\n", data)
	fmt.Printf("             EDN: %s\n", edn)

	var v JSON
	err = cbor.Unmarshal(data, &v)
	if err != nil {
		panic(err)
	}

	fmt.Printf("decoded v.any: %+v\n", v.any)

	for k, v := range v.any.(map[string]interface{}) {
		fmt.Printf("  key: %s, value: %v (%T)\n", k, v, v)
	}
}

Program outputs:

encoded CBOR is 32 bytes because this example includes JSON encoding embedded inside CBOR.
             Hex: d90106581b7b226964223a313032342c226e616d65223a227468656f7279227d
             EDN: 262(h'7b226964223a313032342c226e616d65223a227468656f7279227d')
decoded v.any: map[id:1024 name:theory]
  key: id, value: 1024 (json.Number)
  key: name, value: theory (string)

In general, I prefer not adding extra CBOR tags (defined outside RFC 8949) if this library has API that allows users to add support for those tags (as in your example code with one minor tweak).

Please let me know if this works for you.

@theory
Copy link
Contributor Author

theory commented Apr 20, 2025

Ah, thank you, very nice. Might I suggest adding this or something similar as an example?

@fxamacker
Copy link
Owner

Might I suggest adding this or something similar as an example?

Sounds good! I'll add this or something similar as an example next weekend (April 26-27).

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

No branches or pull requests

2 participants