Skip to content

Commit 59fec32

Browse files
author
Wails Documentation Agent
committed
fix(v2): preserve nested struct JSON structure for embedded types with json tags
When a Go struct has anonymous embedded struct fields with explicit json tags (e.g., `TimeLimitDef `json:"timeLimitDef"``), Go's encoding/json marshals them as nested objects. However, the TS binding generator unconditionally flattened these fields, producing incorrect TypeScript that couldn't deserialize the JSON. Fix deepFields() in typescriptify to check for json tags on anonymous fields before flattening. Fix AddStructToGenerateTS() in binding.go to register embedded struct types with json tags for separate TS class generation. Fixes #4117
1 parent 81018ce commit 59fec32

4 files changed

Lines changed: 132 additions & 6 deletions

File tree

v2/internal/binding/binding.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,19 @@ func (b *Bindings) AddStructToGenerateTS(packageName string, structName string,
271271

272272
for i := 0; i < structType.NumField(); i++ {
273273
field := structType.Field(i)
274-
if field.Anonymous || !field.IsExported() {
274+
if !field.IsExported() {
275275
continue
276276
}
277+
if field.Anonymous {
278+
jsonTag, hasTag := field.Tag.Lookup("json")
279+
if !hasTag {
280+
continue
281+
}
282+
jsonTagParts := strings.Split(jsonTag, ",")
283+
if jsonTagParts[0] == "" || jsonTagParts[0] == "-" {
284+
continue
285+
}
286+
}
277287
kind := field.Type.Kind()
278288
if kind == reflect.Struct {
279289
fqname := field.Type.String()
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package binding_test
2+
3+
type EmbeddedWithJSONTagTimeLimitDef struct {
4+
IsInfinite bool `json:"isInfinite"`
5+
StartTime int64 `json:"startTime"`
6+
EndTime int64 `json:"endTime"`
7+
}
8+
9+
type EmbeddedWithJSONTagApplication struct {
10+
AppName string `json:"appName"`
11+
AppVersion string `json:"appVersion"`
12+
AuthUser string `json:"authUser"`
13+
AuthModule string `json:"authModule"`
14+
}
15+
16+
type StructWithEmbeddedNamedStructJSONTag struct {
17+
Name string `json:"name"`
18+
Typ string `json:"typ"`
19+
Desc string `json:"desc"`
20+
EmbeddedWithJSONTagTimeLimitDef `json:"timeLimitDef"`
21+
EmbeddedWithJSONTagApplication `json:"application"`
22+
}
23+
24+
func (s StructWithEmbeddedNamedStructJSONTag) Get() StructWithEmbeddedNamedStructJSONTag {
25+
return s
26+
}
27+
28+
var EmbeddedNamedStructJSONTagTest = BindingTest{
29+
name: "EmbeddedNamedStructJSONTag",
30+
structs: []interface{}{
31+
&StructWithEmbeddedNamedStructJSONTag{},
32+
},
33+
exemptions: nil,
34+
shouldError: false,
35+
want: `
36+
export namespace binding_test {
37+
export class EmbeddedWithJSONTagApplication {
38+
appName: string;
39+
appVersion: string;
40+
authUser: string;
41+
authModule: string;
42+
static createFrom(source: any = {}) {
43+
return new EmbeddedWithJSONTagApplication(source);
44+
}
45+
constructor(source: any = {}) {
46+
if ('string' === typeof source) source = JSON.parse(source);
47+
this.appName = source["appName"];
48+
this.appVersion = source["appVersion"];
49+
this.authUser = source["authUser"];
50+
this.authModule = source["authModule"];
51+
}
52+
}
53+
export class EmbeddedWithJSONTagTimeLimitDef {
54+
isInfinite: boolean;
55+
startTime: number;
56+
endTime: number;
57+
static createFrom(source: any = {}) {
58+
return new EmbeddedWithJSONTagTimeLimitDef(source);
59+
}
60+
constructor(source: any = {}) {
61+
if ('string' === typeof source) source = JSON.parse(source);
62+
this.isInfinite = source["isInfinite"];
63+
this.startTime = source["startTime"];
64+
this.endTime = source["endTime"];
65+
}
66+
}
67+
export class StructWithEmbeddedNamedStructJSONTag {
68+
name: string;
69+
typ: string;
70+
desc: string;
71+
timeLimitDef: EmbeddedWithJSONTagTimeLimitDef;
72+
application: EmbeddedWithJSONTagApplication;
73+
static createFrom(source: any = {}) {
74+
return new StructWithEmbeddedNamedStructJSONTag(source);
75+
}
76+
constructor(source: any = {}) {
77+
if ('string' === typeof source) source = JSON.parse(source);
78+
this.name = source["name"];
79+
this.typ = source["typ"];
80+
this.desc = source["desc"];
81+
this.timeLimitDef = this.convertValues(source["timeLimitDef"], EmbeddedWithJSONTagTimeLimitDef);
82+
this.application = this.convertValues(source["application"], EmbeddedWithJSONTagApplication);
83+
}
84+
convertValues(a: any, classs: any, asMap: boolean = false): any {
85+
if (!a) {
86+
return a;
87+
}
88+
if (a.slice && a.map) {
89+
return (a as any[]).map(elem => this.convertValues(elem, classs));
90+
} else if ("object" === typeof a) {
91+
if (asMap) {
92+
for (const key of Object.keys(a)) {
93+
a[key] = new classs(a[key]);
94+
}
95+
return a;
96+
}
97+
return new classs(a);
98+
}
99+
return a;
100+
}
101+
}
102+
}
103+
`,
104+
}

v2/internal/binding/binding_test/binding_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func TestBindings_GenerateModels(t *testing.T) {
4646
GeneratedJsEntityWithNestedStructInterfacesTest,
4747
AnonymousSubStructTest,
4848
AnonymousSubStructMultiLevelTest,
49+
EmbeddedNamedStructJSONTagTest,
4950
GeneratedJsEntityWithNestedStructTest,
5051
EntityWithDiffNamespacesTest,
5152
SpecialCharacterFieldTest,

v2/internal/typescriptify/typescriptify.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,24 @@ func (t *TypeScriptify) deepFields(typeOf reflect.Type) []reflect.StructField {
171171
kind := f.Type.Kind()
172172
isPointer := kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct
173173
if f.Anonymous && kind == reflect.Struct {
174-
// fmt.Println(v.Interface())
175-
fields = append(fields, t.deepFields(f.Type)...)
174+
if jsonTag, hasTag := f.Tag.Lookup("json"); hasTag {
175+
jsonTagParts := strings.Split(jsonTag, ",")
176+
if jsonTagParts[0] != "" && jsonTagParts[0] != "-" {
177+
fields = append(fields, f)
178+
}
179+
} else {
180+
fields = append(fields, t.deepFields(f.Type)...)
181+
}
176182
} else if f.Anonymous && isPointer {
177-
// fmt.Println(v.Interface())
178-
fields = append(fields, t.deepFields(f.Type.Elem())...)
183+
if jsonTag, hasTag := f.Tag.Lookup("json"); hasTag {
184+
jsonTagParts := strings.Split(jsonTag, ",")
185+
if jsonTagParts[0] != "" && jsonTagParts[0] != "-" {
186+
fields = append(fields, f)
187+
}
188+
} else {
189+
fields = append(fields, t.deepFields(f.Type.Elem())...)
190+
}
179191
} else {
180-
// Check we have a json tag
181192
jsonTag := t.getJSONFieldName(f, isPointer)
182193
if jsonTag != "" {
183194
fields = append(fields, f)

0 commit comments

Comments
 (0)