fix(api): default into typed nil maps and slices

A nil map or slice held in an interface is not == nil, so the
defaulting walk took it for a present value: writing the first default
into a nil map panicked, and a nil slice kept no default. The registry
always passes a decoded map, but Apply is now a shared entry point and
nothing guarantees that for its other callers.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
This commit is contained in:
Mattia Eleuteri committed 2026-11-09 11:36:14 +01:00
1 parent 89aba6e104
commit 0a310cc507
2 files changed
+56 -2

No files matched your search

+2 -2
View File
@@ -101,7 +101,7 @@ func applyDefaults(v any, s *structuralschema.Structural, top bool) (any, error)
switch effType {
case "object":
mv, isMap := v.(map[string]any)
if !isMap || v == nil {
if !isMap || mv == nil {
if s.Default.Object != nil && !top {
if dm, ok := s.Default.Object.(map[string]any); ok {
mv = cloneMap(dm)
@@ -144,7 +144,7 @@ func applyDefaults(v any, s *structuralschema.Structural, top bool) (any, error)
case "array":
sl, isSlice := v.([]any)
if !isSlice || v == nil {
if !isSlice || sl == nil {
if s.Default.Object != nil {
if ds, ok := s.Default.Object.([]any); ok {
sl = cloneSlice(ds)
@@ -0,0 +1,54 @@
/*
Copyright 2026 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defaulting
import "testing"
// A nil map[string]any in an interface is not == nil, so it has to be
// recognised as empty explicitly, at the root and below it, or writing the
// first default into it panics.
func TestApplyDefaultsTypedNilMaps(t *testing.T) {
s, err := BuildSchema(`{"type":"object","properties":{
"replicas":{"type":"integer","default":2},
"roles":{"type":"array","default":["worker"],"items":{"type":"string"}},
"resources":{"type":"object","default":{},"properties":{"preset":{"type":"string","default":"t1.micro"}}}}}`)
if err != nil {
t.Fatal(err)
}
var root map[string]any
if err := Apply(&root, s); err != nil {
t.Fatal(err)
}
if root["replicas"] != int64(2) {
t.Errorf("root: replicas = %#v, want 2", root["replicas"])
}
var nested map[string]any
var roles []any
root = map[string]any{"resources": nested, "roles": roles}
if err := Apply(&root, s); err != nil {
t.Fatal(err)
}
resources, _ := root["resources"].(map[string]any)
if resources["preset"] != "t1.micro" {
t.Errorf("nested: resources = %#v, want the preset default", root["resources"])
}
if got, _ := root["roles"].([]any); len(got) != 1 || got[0] != "worker" {
t.Errorf("nested: roles = %#v, want the array default", root["roles"])
}
}