Skip to content

Commit 064ea68

Browse files
walidcaveliusclaudeci.datadog-api-spec
authored
fix(generator): empty additionalProperties treated as falsy, breaking nullable map example generation (AAWF-1198) (#3952)
* fix(formatter): handle empty additionalProperties as map[string]interface{} When `additionalProperties: {}` is set on a schema, Python's truthiness evaluates the empty dict as falsy, causing the additionalProperties block to be skipped. The generator then fell through to the nullable path and emitted `*NewNullableXxx(&Xxx{})` — a constructor that is never generated for map schemas — resulting in a Go compile error. Fix the condition to check for the presence of the key and an explicit `false` value rather than relying on truthiness. When additionalProperties is an empty dict or boolean `true`, normalize to `interface{}` as the nested value type and format values inline. Also remove the now-unreachable dead-code branch that previously handled `type: object + additionalProperties: {}` after the nullable check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * pre-commit fixes * fix(formatter): properly escape values in untyped additionalProperties When formatting values for untyped additionalProperties (e.g. additionalProperties: {}), infer the schema from the Python value type so that string escaping, boolean and number formatting are handled correctly. Previously, the fallback `f'"{v}"'` would embed raw newlines in Go string literals, causing compile errors for values like multi-line strings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * pre-commit fixes * fix(formatter): use map type for named map schemas in array items When array items are a $ref to a schema with additionalProperties (and no properties), schema_name() returned the ref name (e.g. IDPConfigValueItem) which was used as the Go type — but no struct is ever generated for map schemas, causing a compile error. Apply the same additionalProperties != False guard used elsewhere so that named map schemas resolve to map[string]T instead of the struct name, consistent with how type_to_go() handles them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: ci.datadog-api-spec <packages@datadoghq.com>
1 parent 605cb35 commit 064ea68

7 files changed

Lines changed: 227 additions & 33 deletions

File tree

.generator/src/generator/formatter.py

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -559,9 +559,18 @@ def format_data_with_schema_list(
559559
nested_schema_name = "interface{}"
560560
else:
561561
if nested_schema_name:
562-
nested_schema_name = f"{name_prefix}{nested_schema_name}"
563-
elif list_schema.get("type") == "object" and list_schema.get("additionalProperties") == {}:
564-
nested_schema_name = "map[string]interface{}"
562+
additional = list_schema.get("additionalProperties", False)
563+
if additional != False and not list_schema.get("properties"):
564+
# Named schema is a map type (no struct generated for it) — use the
565+
# map type directly to avoid referencing a non-existent struct.
566+
value_type = (simple_type(additional) if isinstance(additional, dict) and additional else None) or "interface{}"
567+
nested_schema_name = f"map[string]{value_type}"
568+
else:
569+
nested_schema_name = f"{name_prefix}{nested_schema_name}"
570+
elif list_schema.get("additionalProperties", False) != False:
571+
additional = list_schema.get("additionalProperties")
572+
value_type = (simple_type(additional) if isinstance(additional, dict) and additional else None) or "interface{}"
573+
nested_schema_name = f"map[string]{value_type}"
565574
else:
566575
nested_schema_name = "interface{}"
567576

@@ -642,33 +651,66 @@ def format_data_with_schema_dict(
642651
)
643652
parameters += f"{camel_case(k)}: {value},\n"
644653

645-
if schema.get("additionalProperties"):
654+
additional = schema.get("additionalProperties", False)
655+
if additional != False:
646656
saved_parameters = ""
647657
if schema.get("properties"):
648658
saved_parameters = parameters
649659
parameters = ""
650-
nested_schema = schema["additionalProperties"]
651-
nested_schema_name = simple_type(nested_schema)
652-
if not nested_schema_name:
653-
nested_schema_name = schema_name(nested_schema)
654-
if nested_schema_name:
655-
nested_schema_name = name_prefix + nested_schema_name
656-
elif nested_schema.get("type") is None:
657-
nested_schema_name = "interface{}"
660+
# Typed additionalProperties (non-empty dict): use it as the nested schema.
661+
# Untyped (empty dict or True): any value is allowed, treat as interface{}.
662+
nested_schema = additional if isinstance(additional, dict) and additional else None
663+
if nested_schema:
664+
nested_schema_name = simple_type(nested_schema)
665+
if not nested_schema_name:
666+
nested_schema_name = schema_name(nested_schema)
667+
if nested_schema_name:
668+
nested_schema_name = name_prefix + nested_schema_name
669+
elif nested_schema.get("type") is None:
670+
nested_schema_name = "interface{}"
671+
else:
672+
nested_schema_name = "interface{}"
658673

659674
has_properties = schema.get("properties")
660675

661676
for k, v in data.items():
662677
if has_properties and k in schema["properties"]:
663678
continue
664-
value = format_data_with_schema(
665-
v,
666-
schema["additionalProperties"],
667-
name_prefix=name_prefix,
668-
replace_values=replace_values,
669-
required=True,
670-
**kwargs,
671-
)
679+
if nested_schema:
680+
value = format_data_with_schema(
681+
v,
682+
nested_schema,
683+
name_prefix=name_prefix,
684+
replace_values=replace_values,
685+
required=True,
686+
**kwargs,
687+
)
688+
else:
689+
# Infer schema from the Python value type so primitives are formatted
690+
# correctly (e.g. strings with newlines use backtick literals, booleans
691+
# emit "true"/"false"). bool must come before int because bool is a
692+
# subclass of int in Python — format_interface would otherwise return
693+
# "True"/"False" instead of valid Go literals.
694+
# For complex types (dict, list) inferred stays {}: format_data_with_schema
695+
# short-circuits on an empty schema and returns "", so the fallback
696+
# f'"{v}"' emits a Python repr string. Not ideal but these values were
697+
# previously silently dropped, so this is strictly an improvement.
698+
if isinstance(v, bool):
699+
inferred = {"type": "boolean"}
700+
elif isinstance(v, (int, float)):
701+
inferred = {"type": "number"}
702+
elif isinstance(v, str):
703+
inferred = {"type": "string"}
704+
else:
705+
inferred = {}
706+
value = format_data_with_schema(
707+
v,
708+
inferred,
709+
name_prefix=name_prefix,
710+
replace_values=replace_values,
711+
required=True,
712+
**kwargs,
713+
) or f'"{v}"'
672714
parameters += f'"{k}": {value},\n'
673715

674716
# IMPROVE: find a better way to get nested schema name
@@ -687,14 +729,7 @@ def format_data_with_schema_dict(
687729
return _format_oneof(schema, data, name, name_prefix, replace_values, required, nullable, **kwargs)
688730

689731
if schema.get("type") == "object" and "properties" not in schema:
690-
if schema.get("additionalProperties") == {}:
691-
name_prefix = ""
692-
name = "map[string]interface{}"
693-
reference = ""
694-
for k, v in data.items():
695-
parameters += f'"{k}": "{v}",\n'
696-
else:
697-
return "new(interface{})"
732+
return "new(interface{})"
698733

699734
if not name:
700735
raise ValueError(f"Unnamed schema {schema} for {data}")

.generator/tests/test_formatter.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# coding=utf-8
2+
"""Unit tests for the formatter module."""
3+
4+
from generator.formatter import format_data_with_schema
5+
6+
7+
class SchemaWithRef(dict):
8+
"""A schema dict that carries a $ref, as the generator produces after resolving references."""
9+
10+
def __init__(self, *args, ref=None, **kwargs):
11+
super().__init__(*args, **kwargs)
12+
if ref:
13+
self.__reference__ = {"$ref": ref}
14+
15+
16+
class TestFormatDataWithSchemaAdditionalProperties:
17+
"""Tests for format_data_with_schema with additionalProperties schemas."""
18+
19+
def test_empty_additional_properties_with_nullable_generates_map_literal(self):
20+
"""additionalProperties: {} + nullable: true must not generate NewNullableXxx.
21+
22+
Before the fix, the Python truthiness of {} caused the additionalProperties
23+
block to be skipped, falling through to the nullable path which generated
24+
*NewNullableXxx(&Xxx{}) — a function that is never emitted by the model generator
25+
for map schemas, causing a Go compile error.
26+
"""
27+
schema = SchemaWithRef(
28+
{"nullable": True, "additionalProperties": {}},
29+
ref="#/components/schemas/CustomFields",
30+
)
31+
result = format_data_with_schema({"key": "value"}, schema)
32+
assert result == 'map[string]interface{}{\n"key": "value",\n}'
33+
assert "NewNullable" not in result
34+
35+
def test_empty_additional_properties_with_type_object_generates_map_literal(self):
36+
"""type: object + additionalProperties: {} + nullable: true must not generate NewNullableXxx."""
37+
schema = SchemaWithRef(
38+
{"type": "object", "nullable": True, "additionalProperties": {}},
39+
ref="#/components/schemas/IncidentImpactFieldsObject",
40+
)
41+
result = format_data_with_schema({"field": "val"}, schema)
42+
assert result == 'map[string]interface{}{\n"field": "val",\n}'
43+
assert "NewNullable" not in result
44+
45+
def test_typed_additional_properties_generates_typed_map(self):
46+
"""additionalProperties: {type: string} must generate map[string]string{} as before."""
47+
schema = SchemaWithRef(
48+
{"type": "object", "nullable": True, "additionalProperties": {"type": "string"}},
49+
ref="#/components/schemas/StringMap",
50+
)
51+
result = format_data_with_schema({"k": "v"}, schema)
52+
assert result == 'map[string]string{\n"k": "v",\n}'
53+
54+
55+
class TestFormatDataWithSchemaArrayItems:
56+
"""Tests for format_data_with_schema with array items that are named map schemas."""
57+
58+
def test_named_empty_additional_properties_as_array_item_generates_map_slice(self):
59+
"""Array items that are named map schemas must generate []map[string]interface{}.
60+
61+
Before the fix, schema_name() returned "IDPConfigValueItem" for the item schema,
62+
causing the formatter to emit []datadogV2.IDPConfigValueItem — a type that is never
63+
generated for map schemas, causing a Go compile error.
64+
"""
65+
item_schema = SchemaWithRef(
66+
{"type": "object", "additionalProperties": {}},
67+
ref="#/components/schemas/IDPConfigValueItem",
68+
)
69+
array_schema = {"type": "array", "items": item_schema}
70+
result = format_data_with_schema(
71+
[{"id": "dashboard-1", "displayName": "My Dashboard"}], array_schema
72+
)
73+
assert "IDPConfigValueItem" not in result
74+
assert "map[string]interface{}" in result
75+
76+
def test_named_typed_additional_properties_as_array_item_generates_typed_map_slice(self):
77+
"""Array items that are named typed-map schemas must generate []map[string]string."""
78+
item_schema = SchemaWithRef(
79+
{"type": "object", "additionalProperties": {"type": "string"}},
80+
ref="#/components/schemas/StringMapItem",
81+
)
82+
array_schema = {"type": "array", "items": item_schema}
83+
result = format_data_with_schema([{"k": "v"}], array_schema)
84+
assert "StringMapItem" not in result
85+
assert "map[string]string" in result

examples/v2/actions-datastores/BulkWriteDatastoreItems.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ func main() {
2020
Data: &datadogV2.BulkPutAppsDatastoreItemsRequestData{
2121
Attributes: &datadogV2.BulkPutAppsDatastoreItemsRequestDataAttributes{
2222
Values: []map[string]interface{}{
23-
{
23+
map[string]interface{}{
2424
"id": "cust_3141",
2525
"name": "Johnathan",
2626
},
27-
{
27+
map[string]interface{}{
2828
"id": "cust_3142",
2929
"name": "Mary",
3030
},

examples/v2/app-builder/CreateApp.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,21 @@ func main() {
3636
Properties: datadogV2.ComponentProperties{
3737
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
3838
Bool: datadog.PtrBool(true)},
39+
AdditionalProperties: map[string]interface{}{
40+
"content": "# Cat Facts",
41+
"contentType": "markdown",
42+
"textAlign": "left",
43+
"verticalAlign": "top",
44+
},
3945
},
4046
Events: []datadogV2.AppBuilderEvent{},
4147
},
4248
},
4349
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
4450
String: datadog.PtrString("true")},
51+
AdditionalProperties: map[string]interface{}{
52+
"layout": "{'default': {'x': 0, 'y': 0, 'width': 4, 'height': 5}}",
53+
},
4554
},
4655
Events: []datadogV2.AppBuilderEvent{},
4756
},
@@ -56,12 +65,29 @@ func main() {
5665
Properties: datadogV2.ComponentProperties{
5766
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
5867
Bool: datadog.PtrBool(true)},
68+
AdditionalProperties: map[string]interface{}{
69+
"data": "${fetchFacts?.outputs?.body?.data}",
70+
"columns": "[{'dataPath': 'fact', 'header': 'fact', 'isHidden': False, 'id': '0ae2ae9e-0280-4389-83c6-1c5949f7e674'}, {'dataPath': 'length', 'header': 'length', 'isHidden': True, 'id': 'c9048611-0196-4a00-9366-1ef9e3ec0408'}, {'id': '8fa9284b-7a58-4f13-9959-57b7d8a7fe8f', 'dataPath': 'Due Date', 'header': 'Unused Old Column', 'disableSortBy': False, 'formatter': {'type': 'formatted_time', 'format': 'LARGE_WITHOUT_TIME'}, 'isDeleted': True}]",
71+
"summary": true,
72+
"pageSize": "${pageSize?.value}",
73+
"paginationType": "server_side",
74+
"isLoading": "${fetchFacts?.isLoading}",
75+
"rowButtons": "[]",
76+
"isWrappable": false,
77+
"isScrollable": "vertical",
78+
"isSubRowsEnabled": false,
79+
"globalFilter": false,
80+
"totalCount": "${fetchFacts?.outputs?.body?.total}",
81+
},
5982
},
6083
Events: []datadogV2.AppBuilderEvent{},
6184
},
6285
},
6386
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
6487
String: datadog.PtrString("true")},
88+
AdditionalProperties: map[string]interface{}{
89+
"layout": "{'default': {'x': 0, 'y': 5, 'width': 12, 'height': 96}}",
90+
},
6591
},
6692
Events: []datadogV2.AppBuilderEvent{},
6793
},
@@ -76,12 +102,23 @@ func main() {
76102
Properties: datadogV2.ComponentProperties{
77103
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
78104
Bool: datadog.PtrBool(true)},
105+
AdditionalProperties: map[string]interface{}{
106+
"content": `## Random Fact
107+
108+
${randomFact?.outputs?.fact}`,
109+
"contentType": "markdown",
110+
"textAlign": "left",
111+
"verticalAlign": "top",
112+
},
79113
},
80114
Events: []datadogV2.AppBuilderEvent{},
81115
},
82116
},
83117
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
84118
String: datadog.PtrString("true")},
119+
AdditionalProperties: map[string]interface{}{
120+
"layout": "{'default': {'x': 0, 'y': 101, 'width': 12, 'height': 16}}",
121+
},
85122
},
86123
Events: []datadogV2.AppBuilderEvent{},
87124
},
@@ -96,17 +133,34 @@ func main() {
96133
Properties: datadogV2.ComponentProperties{
97134
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
98135
Bool: datadog.PtrBool(true)},
136+
AdditionalProperties: map[string]interface{}{
137+
"label": "Increase Page Size",
138+
"level": "default",
139+
"isPrimary": true,
140+
"isBorderless": false,
141+
"isLoading": false,
142+
"isDisabled": false,
143+
"iconLeft": "angleUp",
144+
"iconRight": "",
145+
},
99146
},
100147
Events: []datadogV2.AppBuilderEvent{
101148
{
102149
Name: datadogV2.APPBUILDEREVENTNAME_CLICK.Ptr(),
103150
Type: datadogV2.APPBUILDEREVENTTYPE_SETSTATEVARIABLEVALUE.Ptr(),
151+
AdditionalProperties: map[string]interface{}{
152+
"variableName": "pageSize",
153+
"value": "${pageSize?.value + 1}",
154+
},
104155
},
105156
},
106157
},
107158
},
108159
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
109160
String: datadog.PtrString("true")},
161+
AdditionalProperties: map[string]interface{}{
162+
"layout": "{'default': {'x': 10, 'y': 134, 'width': 2, 'height': 4}}",
163+
},
110164
},
111165
Events: []datadogV2.AppBuilderEvent{},
112166
},
@@ -121,17 +175,34 @@ func main() {
121175
Properties: datadogV2.ComponentProperties{
122176
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
123177
Bool: datadog.PtrBool(true)},
178+
AdditionalProperties: map[string]interface{}{
179+
"label": "Decrease Page Size",
180+
"level": "default",
181+
"isPrimary": true,
182+
"isBorderless": false,
183+
"isLoading": false,
184+
"isDisabled": false,
185+
"iconLeft": "angleDown",
186+
"iconRight": "",
187+
},
124188
},
125189
Events: []datadogV2.AppBuilderEvent{
126190
{
127191
Name: datadogV2.APPBUILDEREVENTNAME_CLICK.Ptr(),
128192
Type: datadogV2.APPBUILDEREVENTTYPE_SETSTATEVARIABLEVALUE.Ptr(),
193+
AdditionalProperties: map[string]interface{}{
194+
"variableName": "pageSize",
195+
"value": "${pageSize?.value - 1}",
196+
},
129197
},
130198
},
131199
},
132200
},
133201
IsVisible: &datadogV2.ComponentPropertiesIsVisible{
134202
String: datadog.PtrString("true")},
203+
AdditionalProperties: map[string]interface{}{
204+
"layout": "{'default': {'x': 10, 'y': 138, 'width': 2, 'height': 4}}",
205+
},
135206
},
136207
Events: []datadogV2.AppBuilderEvent{},
137208
},

examples/v2/events/CreateEvent.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ func main() {
3838
},
3939
},
4040
NewValue: map[string]interface{}{
41-
"enabled": "True",
41+
"enabled": true,
4242
"percentage": "50%",
4343
"rule": "{'datacenter': 'devcycle.us1.prod'}",
4444
},
4545
PrevValue: map[string]interface{}{
46-
"enabled": "True",
46+
"enabled": true,
4747
"percentage": "10%",
4848
"rule": "{'datacenter': 'devcycle.us1.prod'}",
4949
},

examples/v2/fleet-automation/CreateFleetDeploymentConfigure.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func main() {
2323
Patch: map[string]interface{}{
2424
"apm_config": "{'enabled': True}",
2525
"log_level": "debug",
26-
"logs_enabled": "True",
26+
"logs_enabled": true,
2727
},
2828
},
2929
},

0 commit comments

Comments
 (0)