Skip to content

types.UnitBytes: MarshalJSON/MarshalYAML produce strings but no corresponding unmarshalers exist, breaking round-trips #865

@Venkatesh1505

Description

@Venkatesh1505

Description

types.UnitBytes has custom MarshalJSON and MarshalYAML methods that serialize the value as a string, but no UnmarshalJSON or UnmarshalYAML methods. Since UnitBytes is type UnitBytes int64, Go's default unmarshalers expect a number and reject the string output from the custom marshalers.

This means any JSON or YAML round-trip of a types.UnitBytes value fails.

Minimal reproduction

package main

import (
	"encoding/json"
	"fmt"

	"github.com/compose-spec/compose-go/v2/types"
	"gopkg.in/yaml.v3"
)

func main() {
	original := types.UnitBytes(655360)
	fmt.Printf("Original value: %d (type: %T)\n\n", original, original)

	// JSON round-trip fails
	jsonBytes, _ := json.Marshal(original)
	fmt.Printf("json.Marshal output: %s\n", jsonBytes)

	var jsonResult types.UnitBytes
	err := json.Unmarshal(jsonBytes, &jsonResult)
	fmt.Printf("json.Unmarshal error: %v\n\n", err)

	// YAML round-trip through untyped map
	yamlBytes, _ := yaml.Marshal(original)
	fmt.Printf("yaml.Marshal output: %s", yamlBytes)

	var untypedValue interface{}
	yaml.Unmarshal(yamlBytes, &untypedValue)
	fmt.Printf("yaml.Unmarshal into interface{}: %v (type: %T)\n\n", untypedValue, untypedValue)

	// Untyped value back through JSON also fails
	jsonBytes2, _ := json.Marshal(untypedValue)
	fmt.Printf("json.Marshal of untyped value: %s\n", jsonBytes2)

	var jsonResult2 types.UnitBytes
	err = json.Unmarshal(jsonBytes2, &jsonResult2)
	fmt.Printf("json.Unmarshal back to UnitBytes error: %v\n", err)
}

Output:

Original value: 655360 (type: types.UnitBytes)

json.Marshal output: "655360"
json.Unmarshal error: json: cannot unmarshal string into Go value of type types.UnitBytes

yaml.Marshal output: "655360"
yaml.Unmarshal into interface{}: 655360 (type: string)

json.Marshal of untyped value: "655360"
json.Unmarshal back to UnitBytes error: json: cannot unmarshal string into Go value of type types.UnitBytes

Root cause:

In types/bytes.go, MarshalJSON returns a quoted string and MarshalYAML returns a string:

func (u UnitBytes) MarshalJSON() ([]byte, error) {
    return []byte(fmt.Sprintf(`"%d"`, u)), nil  // "655360" — string in JSON
}

func (u UnitBytes) MarshalYAML() (interface{}, error) {
    return fmt.Sprintf("%d", u), nil  // "655360" — string
}

But there are no UnmarshalJSON or UnmarshalYAML methods, so Go falls back to the default int64 unmarshaler which rejects strings.

The existing DecodeMapstructure method handles both int and string, but that only works with the mapstructure library — not with encoding/json or gopkg.in/yaml.v3.

Context: This issue was discovered via score-spec/score-compose#470, where JSON/YAML round-tripping of types.Project fails when the project contains UnitBytes values.

Suggested fix:

Add UnmarshalJSON and UnmarshalYAML methods that accept both numeric and string values, consistent with the existing DecodeMapstructure logic. Happy to submit a PR if this approach works.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions