Repository: fxamacker/cbor
Pull request #659 (2025-04-26 17:56:37)

Update docs for TimeMode, Tag, RawTag, and add example for Embedded JSON Tag for CBOR
Related to #657 #656

This PR updates some comments and adds a requested example. The example shows how to user apps can use almost any current or future CBOR tag number by implementing `cbor.Marshaler` and `cbor.Unmarshaler` interfaces.

Essentially, `MarshalCBOR` and `UnmarshalCBOR` implemented by user apps are automatically called by this CBOR codec's `Marshal`, `Unmarshal`, etc.

The example encodes and decodes a tagged CBOR data item with tag number 262 and the tag content is a JSON object that is "embedded" as a CBOR byte string (major type 2).

> [!NOTE]
> [RFC 8949](https://www.rfc-editor.org/rfc/rfc8949.html) does not mention CBOR tag number 262.  IANA assigned CBOR tag number 262 to "Embedded JSON Object" and references the document [Embedded JSON Tag for CBOR](https://github.com/toravir/CBOR-Tag-Specs/blob/master/embeddedJSON.md).

Changes:
- [x] Update comments/docs for `cbor.TimeMode` options.
- [x] Update comments/docs for `cbor.Tag` and `cbor.RawTag`.
- [x] Add a [requested example](https://github.com/fxamacker/cbor/issues/657#issuecomment-2817519781) for using [Embedded JSON Tag for CBOR](https://github.com/toravir/CBOR-Tag-Specs/blob/master/embeddedJSON.md) (CBOR tag 262).
  - See file: example_embedded_json_tag_for_cbor_test.go

Thanks for your patience. :pray:


Patch:
diff --git a/encode.go b/encode.go
index a3f9992..c3daf73 100644
--- a/encode.go
+++ b/encode.go
@@ -293,24 +293,51 @@ func (icm InfConvertMode) valid() bool {
 	return icm >= 0 && icm < maxInfConvert
 }
 
-// TimeMode specifies how to encode time.Time values.
+// TimeMode specifies how to encode time.Time values in compliance with RFC 8949 (CBOR):
+// - Section 3.4.1: Standard Date/Time String
+// - Section 3.4.2: Epoch-Based Date/Time
+// For more info, see:
+// - https://www.rfc-editor.org/rfc/rfc8949.html
+// NOTE: User applications that prefer to encode time with fractional seconds to an integer
+// (instead of floating point or text string) can use a CBOR tag number not assigned by IANA:
+//  1. Define a user-defined type in Go with just a time.Time or int64 as its data.
+//  2. Implement the cbor.Marshaler and cbor.Unmarshaler interface for that user-defined type
+//     to encode or decode the tagged data item with an enclosed integer content.
 type TimeMode int
 
 const (
-	// TimeUnix causes time.Time to be encoded as epoch time in integer with second precision.
+	// TimeUnix causes time.Time to encode to a CBOR time (tag 1) with an integer content
+	// representing seconds elapsed (with 1-second precision) since UNIX Epoch UTC.
+	// The TimeUnix option is location independent and has a clear precision guarantee.
 	TimeUnix TimeMode = iota
 
-	// TimeUnixMicro causes time.Time to be encoded as epoch time in float-point rounded to microsecond precision.
+	// TimeUnixMicro causes time.Time to encode to a CBOR time (tag 1) with a floating point content
+	// representing seconds elapsed (with up to 1-microsecond precision) since UNIX Epoch UTC.
+	// NOTE: The floating point content is encoded to the shortest floating-point encoding that preserves
+	// the 64-bit floating point value. I.e., the floating point encoding can be IEEE 764:
+	// binary64, binary32, or binary16 depending on the content's value.
 	TimeUnixMicro
 
-	// TimeUnixDynamic causes time.Time to be encoded as integer if time.Time doesn't have fractional seconds,
-	// otherwise float-point rounded to microsecond precision.
+	// TimeUnixDynamic causes time.Time to encode to a CBOR time (tag 1) with either an integer content or
+	// or a floating point content, depending on the content's value.  This option is equivalent to dynamically
+	// choosing TimeUnix if time.Time doesn't have fractional seconds, and using TimeUnixMicro if time.Time
+	// has fractional seconds.
 	TimeUnixDynamic
 
-	// TimeRFC3339 causes time.Time to be encoded as RFC3339 formatted string with second precision.
+	// TimeRFC3339 causes time.Time to encode to a CBOR time (tag 0) with a text string content
+	// representing the time using 1-second precision in RFC3339 format.  If the time.Time has a
+	// non-UTC timezone then a "localtime - UTC" numeric offset will be included as specified in RFC3339.
+	// NOTE: User applications can avoid including the RFC3339 numeric offset by:
+	// - providing a time.Time value set to UTC, or
+	// - using the TimeUnix, TimeUnixMicro, or TimeUnixDynamic option instead of TimeRFC3339.
 	TimeRFC3339
 
-	// TimeRFC3339Nano causes time.Time to be encoded as RFC3339 formatted string with nanosecond precision.
+	// TimeRFC3339Nano causes time.Time to encode to a CBOR time (tag 0) with a text string content
+	// representing the time using 1-nanosecond precision in RFC3339 format.  If the time.Time has a
+	// non-UTC timezone then a "localtime - UTC" numeric offset will be included as specified in RFC3339.
+	// NOTE: User applications can avoid including the RFC3339 numeric offset by:
+	// - providing a time.Time value set to UTC, or
+	// - using the TimeUnix, TimeUnixMicro, or TimeUnixDynamic option instead of TimeRFC3339Nano.
 	TimeRFC3339Nano
 
 	maxTimeMode
diff --git a/tag.go b/tag.go
index 47bcca8..bd8b773 100644
--- a/tag.go
+++ b/tag.go
@@ -10,22 +10,23 @@ import (
 	"sync"
 )
 
-// Tag represents CBOR tag data, including tag number and unmarshaled tag content. Marshaling and
-// unmarshaling of tag content is subject to any encode and decode options that would apply to
-// enclosed data item if it were to appear outside of a tag.
+// Tag represents a tagged data item (CBOR major type 6), comprising a tag number and the unmarshaled tag content.
+// NOTE: The same encoding and decoding options that apply to untagged CBOR data items also applies to tag content
+// during encoding and decoding.
 type Tag struct {
 	Number  uint64
 	Content any
 }
 
-// RawTag represents CBOR tag data, including tag number and raw tag content.
-// RawTag implements Unmarshaler and Marshaler interfaces.
+// RawTag represents a tagged data item (CBOR major type 6), comprising a tag number and the raw tag content.
+// The raw tag content (enclosed data item) is a CBOR-encoded data item.
+// RawTag can be used to delay decoding a CBOR data item or precompute encoding a CBOR data item.
 type RawTag struct {
 	Number  uint64
 	Content RawMessage
 }
 
-// UnmarshalCBOR sets *t with tag number and raw tag content copied from data.
+// UnmarshalCBOR sets *t with the tag number and the raw tag content copied from data.
 //
 // Deprecated: No longer used by this codec; kept for compatibility
 // with user apps that directly call this function.
@@ -49,7 +50,7 @@ func (t *RawTag) UnmarshalCBOR(data []byte) error {
 	return t.unmarshalCBOR(data)
 }
 
-// unmarshalCBOR sets *t with tag number and raw tag content copied from data.
+// unmarshalCBOR sets *t with the tag number and the raw tag content copied from data.
 // This function assumes data is well-formed, and does not perform bounds checking.
 // This function is called by Unmarshal().
 func (t *RawTag) unmarshalCBOR(data []byte) error {


Test patch:
diff --git a/example_embedded_json_tag_for_cbor_test.go b/example_embedded_json_tag_for_cbor_test.go
new file mode 100644
index 0000000..29ee431
--- /dev/null
+++ b/example_embedded_json_tag_for_cbor_test.go
@@ -0,0 +1,130 @@
+// Copyright (c) Faye Amacker. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+package cbor_test
+
+// fxamacker/cbor allows user apps to use almost any current or future
+// CBOR tag number by implementing cbor.Marshaler and cbor.Unmarshaler
+// interfaces.  Essentially, MarshalCBOR and UnmarshalCBOR functions that
+// are implemented by user apps will automatically be called by this
+// CBOR codec's Marshal, Unmarshal, etc.
+//
+// This example shows how to encode and decode a tagged CBOR data item with
+// tag number 262 and the tag content is a JSON object "embedded" as a
+// CBOR byte string (major type 2).
+//
+// NOTE: RFC 8949 does not mention tag number 262. IANA assigned
+// CBOR tag number 262 as "Embedded JSON Object" specified by the
+// document Embedded JSON Tag for CBOR:
+//
+//	"Tag 262 can be applied to a byte string (major type 2) to indicate
+//	that the byte string is a JSON Object. The length of the byte string
+//	indicates the content."
+//
+// For more info, see Embedded JSON Tag for CBOR at:
+// https://github.com/toravir/CBOR-Tag-Specs/blob/master/embeddedJSON.md
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+
+	"github.com/fxamacker/cbor/v2"
+)
+
+// cborTagNumForEmbeddedJSON is the CBOR tag number 262.
+const cborTagNumForEmbeddedJSON = 262
+
+// EmbeddedJSON represents a Go value to be encoded as a tagged CBOR data item
+// with tag number 262 and the tag content is a JSON object "embedded" as a
+// CBOR byte string (major type 2).
+type EmbeddedJSON struct {
+	any
+}
+
+func NewEmbeddedJSON(val any) EmbeddedJSON {
+	return EmbeddedJSON{val}
+}
+
+// MarshalCBOR encodes EmbeddedJSON to a tagged CBOR data item with the
+// tag number 262 and the tag content is a JSON object that is
+// "embedded" as a CBOR byte string.
+func (v EmbeddedJSON) MarshalCBOR() ([]byte, error) {
+	// Encode v to JSON object.
+	data, err := json.Marshal(v)
+	if err != nil {
+		return nil, err
+	}
+
+	// Create cbor.Tag representing a tagged CBOR data item.
+	tag := cbor.Tag{
+		Number:  cborTagNumForEmbeddedJSON,
+		Content: data,
+	}
+
+	// Marshal to a tagged CBOR data item.
+	return cbor.Marshal(tag)
+}
+
+// UnmarshalCBOR decodes a tagged CBOR data item to EmbeddedJSON.
+// The byte slice provided to this function must contain a single
+// tagged CBOR data item with the tag number 262 and tag content
+// must be a JSON object "embedded" as a CBOR byte string.
+func (v *EmbeddedJSON) UnmarshalCBOR(b []byte) error {
+	// Unmarshal tagged CBOR data item.
+	var tag cbor.Tag
+	if err := cbor.Unmarshal(b, &tag); err != nil {
+		return err
+	}
+
+	// Check tag number.
+	if tag.Number != cborTagNumForEmbeddedJSON {
+		return fmt.Errorf("got tag number %d, expect tag number %d", tag.Number, cborTagNumForEmbeddedJSON)
+	}
+
+	// Check tag content.
+	jsonData, isByteString := tag.Content.([]byte)
+	if !isByteString {
+		return fmt.Errorf("got tag content type %T, expect tag content []byte", tag.Content)
+	}
+
+	// Unmarshal JSON object.
+	return json.Unmarshal(jsonData, v)
+}
+
+// MarshalJSON encodes EmbeddedJSON to a JSON object.
+func (v EmbeddedJSON) MarshalJSON() ([]byte, error) {
+	return json.Marshal(v.any)
+}
+
+// UnmarshalJSON decodes a JSON object.
+func (v *EmbeddedJSON) UnmarshalJSON(b []byte) error {
+	dec := json.NewDecoder(bytes.NewReader(b))
+	dec.UseNumber()
+	return dec.Decode(&v.any)
+}
+
+func Example_embeddedJSONTagForCBOR() {
+	value := NewEmbeddedJSON(map[string]any{
+		"name": "gopher",
+		"id":   json.Number("42"),
+	})
+
+	data, err := cbor.Marshal(value)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("cbor: %x\n", data)
+
+	var v EmbeddedJSON
+	err = cbor.Unmarshal(data, &v)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("%+v\n", v.any)
+	for k, v := range v.any.(map[string]any) {
+		fmt.Printf("  %s: %v (%T)\n", k, v, v)
+	}
+}