Describe the bug
When multiple patch templates are loaded (via --patch-templates specified multiple times or via a directory), generate fails with a JSON unmarshal error if any earlier template sets a value that uses types.UnitBytes (e.g., tmpfs.size).
Error: failed to patch template 2: failed to unmarshal patched output: json: cannot unmarshal string
into Go struct field ServiceVolumeTmpfs.services.volumes.tmpfs.size of type types.UnitBytes
A single patch template works fine. The bug only triggers when a second template is applied after the first.
To Reproduce
-
Create a minimal score.yaml:
apiVersion: score.dev/v1b1
metadata:
name: example
containers:
hello-world:
image: nginx:latest
service:
ports:
www:
port: 8080
targetPort: 80
-
Init with two patch templates where the first sets a tmpfs volume (e.g., using community patchers):
score-compose init --no-sample \
--patch-templates ../community-patchers/score-compose/microcks.tpl \
--patch-templates ../community-patchers/score-compose/ollama.tpl
-
Run generate:
score-compose generate score.yaml
-
See error:
Error: failed to patch template 2: failed to unmarshal patched output: json: cannot unmarshal string
into Go struct field ServiceVolumeTmpfs.services.volumes.tmpfs.size of type types.UnitBytes
Expected behavior
Both patch templates should be applied successfully and compose.yaml should be generated with both microcks and ollama services.
Screenshots
Running with -vv shows template 1 succeeds but template 2 fails during unmarshal:
Applying patching template 1
Applying patch operation=set path=services.microcks value="map[...volumes:[map[target:/tmp tmpfs:map[size:655360] type:tmpfs]]]"
Applying patching template 2
Applying patch operation=set path=services.ollama value="map[image:ollama/ollama:latest ...]"
Applying patch operation=set path=volumes.ollama_data value="map[driver:local name:ollama_data]"
Error: failed to patch template 2: failed to unmarshal patched output: json: cannot unmarshal string
into Go struct field ServiceVolumeTmpfs.services.volumes.tmpfs.size of type types.UnitBytes
Note that template 1's patch sets size:655360 as a numeric value. The error occurs when template 2 tries to unmarshal the result back into *types.Project - by that point, the size value has been corrupted from a number to a string during the YAML round-trip between the two templates.
Desktop (please complete the following information):
- OS: macOS
- Version: built from source (main branch)
Additional context
Root cause analysis:
PatchServices (internal/patching/patching.go) is called once per template in a loop (internal/command/generate.go:293-298). Each call performs:
yamlRoundTrip: *types.Project → yaml.Marshal → yaml.Unmarshal into map[string]interface{}
json.Marshal the untyped map
- Apply patches via
sjson
json.Unmarshal back into *types.Project
The corruption happens at step 1 on the second template. types.UnitBytes in compose-go (types/bytes.go) has a custom MarshalYAML that serializes the numeric value as a string:
func (u UnitBytes) MarshalYAML() (interface{}, error) {
return fmt.Sprintf("%d", u), nil // returns "655360" (string, not int)
}
When this YAML is unmarshaled into map[string]interface{}, the value becomes a Go string("655360") instead of a number. This string flows through json.Marshal → sjson → json.Unmarshal, and the final unmarshal into *types.Project fails because UnitBytes is type UnitBytes int64 with no custom UnmarshalJSON — Go's default int64 unmarshaler rejects the string.
Similarly, MarshalJSON also serializes as a quoted string:
func (u UnitBytes) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%d"`, u)), nil // returns "655360" (string in JSON)
}
The type has a DecodeMapstructure method that handles both int and string, but that's only used by the mapstructure library (compose-go's internal loading path), not by encoding/json or gopkg.in/yaml.v3.
This asymmetry was introduced in compose-spec/compose-go#482 to fix docker/compose#11160.
Suggested fix:
The fix should be upstream in compose-go - add UnmarshalJSON and UnmarshalYAML to types.UnitBytes that accept both numeric and string values, mirroring the existing DecodeMapstructure logic.
Describe the bug
When multiple patch templates are loaded (via
--patch-templatesspecified multiple times or via a directory),generatefails with a JSON unmarshal error if any earlier template sets a value that usestypes.UnitBytes(e.g.,tmpfs.size).A single patch template works fine. The bug only triggers when a second template is applied after the first.
To Reproduce
Create a minimal
score.yaml:Init with two patch templates where the first sets a
tmpfsvolume (e.g., using community patchers):Run generate:
See error:
Expected behavior
Both patch templates should be applied successfully and
compose.yamlshould be generated with bothmicrocksandollamaservices.Screenshots
Running with
-vvshows template 1 succeeds but template 2 fails during unmarshal:Note that template 1's patch sets
size:655360as a numeric value. The error occurs when template 2 tries to unmarshal the result back into*types.Project- by that point, thesizevalue has been corrupted from a number to a string during the YAML round-trip between the two templates.Desktop (please complete the following information):
Additional context
Root cause analysis:
PatchServices(internal/patching/patching.go) is called once per template in a loop (internal/command/generate.go:293-298). Each call performs:yamlRoundTrip:*types.Project→yaml.Marshal→yaml.Unmarshalintomap[string]interface{}json.Marshalthe untyped mapsjsonjson.Unmarshalback into*types.ProjectThe corruption happens at step 1 on the second template.
types.UnitBytesin compose-go (types/bytes.go) has a customMarshalYAMLthat serializes the numeric value as a string:When this YAML is unmarshaled into
map[string]interface{}, the value becomes a Gostring("655360")instead of a number. This string flows throughjson.Marshal→sjson→json.Unmarshal, and the final unmarshal into*types.Projectfails becauseUnitBytesistype UnitBytes int64with no customUnmarshalJSON— Go's defaultint64unmarshaler rejects the string.Similarly,
MarshalJSONalso serializes as a quoted string:The type has a
DecodeMapstructuremethod that handles bothintandstring, but that's only used by the mapstructure library (compose-go's internal loading path), not byencoding/jsonorgopkg.in/yaml.v3.This asymmetry was introduced in compose-spec/compose-go#482 to fix docker/compose#11160.
Suggested fix:
The fix should be upstream in compose-go - add
UnmarshalJSONandUnmarshalYAMLtotypes.UnitBytesthat accept both numeric and string values, mirroring the existingDecodeMapstructurelogic.