Skip to content

fix: parse schema for complex struct#47

Merged
ren0503 merged 1 commit into
masterfrom
fix/ren/46-complex-struct
Oct 26, 2025
Merged

fix: parse schema for complex struct#47
ren0503 merged 1 commit into
masterfrom
fix/ren/46-complex-struct

Conversation

@ren0503

@ren0503 ren0503 commented Oct 26, 2025

Copy link
Copy Markdown
Contributor

No description provided.

@ren0503 ren0503 added this to the Swagger Release v2.2.2 milestone Oct 26, 2025
@ren0503 ren0503 linked an issue Oct 26, 2025 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Oct 26, 2025

Copy link
Copy Markdown

Summary by CodeRabbit

  • Improvements
    • Enhanced OpenAPI schema generation for complex nested structures and multi-level arrays
    • Improved validation rule handling and automatic required field detection in schemas
    • Added proper support for time type formatting in generated API definitions
    • Refined type mapping logic for struct-based data models
    • Comprehensive test coverage for complex nested schema parsing scenarios

Walkthrough

The PR refactors schema parsing to comprehensively handle struct-based DTOs with nested field support, JSON tag resolution, required field detection, and proper OpenAPI type mappings. New helper functions enable recursive parsing of nested structs and arrays while supporting validation tags, time formatting, and example metadata.

Changes

Cohort / File(s) Summary
Schema Parsing Logic
parse.go
Rewrites ParseSchema with complete struct field iteration, JSON tag resolution, and exclusion of unexported fields. Adds helper functions: parseJSONName for JSON tag extraction, isTimeType for time.Time detection, parseNested for recursive nested struct/array parsing. Implements required field collection via validate tags and example handling for primitives and arrays.
Type Definitions
type.go, utils.go
Updates ItemsObject: changes Required from string to []string, adds Properties field (map[string]*SchemaObject). Modifies mappingType to treat reflect.Struct as "object" type.
Test Coverage
parse_test.go
Introduces new test structs (CreateTimeoffTypeInput, RequiredInfoInput, UpsertConfigInput, etc.) with nested validation fields and sub-structs. Adds Test_ComplexStruct test that validates complex DTO schema generation with JSON marshaling assertions.

Sequence Diagram

sequenceDiagram
    participant User as Caller
    participant PS as ParseSchema()
    participant Helper as Helper Functions
    participant Reflect as Reflect Package
    
    User->>PS: Call ParseSchema(dto struct)
    PS->>Reflect: Reflect.TypeOf(dto)
    
    rect rgb(200, 220, 240)
    Note over PS,Helper: Pointer dereferencing & struct validation
    PS->>Helper: isTimeType()
    Helper-->>PS: Check time.Time
    end
    
    PS->>Reflect: Iterate struct fields
    
    rect rgb(220, 240, 220)
    Note over PS,Helper: Field processing loop
    Reflect-->>PS: For each field
    PS->>Helper: parseJSONName(field.Tag)
    Helper-->>PS: Extract JSON name
    PS->>Helper: parseNested(field)
    Helper-->>PS: Recursive schema/Items
    PS->>PS: Collect Required fields
    PS->>PS: Build Properties map
    end
    
    PS-->>User: Return SchemaObject with Properties & Required
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • parse.go: The rewrite introduces multiple new helper functions (parseJSONName, isTimeType, parseNested) with recursive logic for nested structs and arrays—requires careful validation of field iteration, JSON tag parsing correctness, and nested schema construction.
  • parse_test.go: New complex test structs with nested validations and assertions on JSON output—verify test struct definitions align with implementation assumptions and assertions are comprehensive.
  • type.go: Field type changes to ItemsObject (Required and Properties)—confirm compatibility with array schema generation and existing code using these fields.
  • utils.go: Minor type mapping addition—low complexity but verify reflect.Struct mapping doesn't conflict with pointer handling elsewhere.

Possibly related issues

  • Parse Schema For Complex Struct #46: Implements comprehensive nested-struct parsing with JSON tag resolution and array/Items support that directly addresses the complex DTO handling requirements described in the issue.

Poem

🐰 Nested structs within our schemas deep,
JSON tags their secrets keep,
Fields recursive, validation true,
OpenAPI paths shine anew!
Hop along, the parsing's clean~ 🌱

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning No pull request description was provided by the author. While the check is lenient regarding detail level, the pass criterion requires the description to be "related in some way to the changeset." An empty description cannot satisfy this basic requirement, as it does not describe any part of the changeset. The fail criterion also explicitly includes descriptions that "do not describe any part of the changeset," which applies to an absent description.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "fix: parse schema for complex struct" is directly related to the main change in the changeset. The summary shows that the primary changes involve a comprehensive rewrite of the ParseSchema function to handle complex struct-based DTOs, along with supporting changes to type definitions and utility functions. The title accurately captures this core objective in a concise, clear manner without vague terminology or excessive detail.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ren/46-complex-struct

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.00000% with 27 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
parse.go 63.51% 22 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
utils.go (1)

35-57: Pointer types are mapped as "object"; dereference before mapping.

Current logic reports *int, *string, *time.Time, *[]T as "object". This produces incorrect schemas for optional primitives and arrays and breaks time formatting. Fix by unwrapping pointers first, then mapping the underlying kind.

Apply this diff to make mapping robust:

 func mappingType(val reflect.Type) string {
-	if val == reflect.TypeOf(time.Time{}) {
-		return "string"
-	}
-	switch val.Kind() {
+	if val == nil {
+		return ""
+	}
+	// Unwrap pointers (handles *T, **T, *time.Time, *[]T, etc.)
+	for val.Kind() == reflect.Ptr {
+		val = val.Elem()
+	}
+	if val == reflect.TypeOf(time.Time{}) {
+		return "string"
+	}
+	switch val.Kind() {
 	case reflect.Bool:
 		return "boolean"
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		return "integer"
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 		return "integer"
 	case reflect.Float32, reflect.Float64:
 		return "number"
 	case reflect.String:
 		return "string"
-	case reflect.Pointer, reflect.Struct:
+	case reflect.Struct:
 		return "object"
 	case reflect.Slice, reflect.Array:
 		return "array"
 	default:
 		return ""
 	}
 }
parse.go (2)

52-58: RequestBody content map key must be a media type, not a schema name.

OpenAPI 3.0 uses MIME types (e.g., "application/json") as keys under content. Using struct names is non‑compliant.

-				mediaTypes[common.GetStructName(val)] = &MediaTypeObject{
+				mediaTypes["application/json"] = &MediaTypeObject{
 					Schema: &SchemaObject{
 						Ref: "#/components/schemas/" + common.GetStructName(val),
 					},
 				}

169-172: Guard against nil Components to avoid panic.

spec.Components can be nil; assigning Schemas will panic.

-	// spec.Definitions = definitions
-	spec.Components.Schemas = schemas
+	// ensure components exists
+	if spec.Components == nil {
+		spec.Components = &ComponentObject{}
+	}
+	spec.Components.Schemas = schemas
 	spec.Paths = pathObject
🧹 Nitpick comments (4)
type.go (1)

98-103: Expose ItemsObject.Required but parser doesn’t populate it for array-of-structs.

You added ItemsObject.Required []string and Properties, but parseNested doesn’t set Items.Required for arrays of structs. Populate it so item-level required fields are preserved.

Follow-up is in parse.go suggestions where we set Items.Required = inner.Required. Based on learnings.

parse_test.go (1)

61-61: Remove debug print from tests.

fmt.Printf in tests adds noise and can mask issues in CI logs.

-	fmt.Printf("%+v\n", defintion.Properties)
+	// no-op: avoid debug prints in tests
parse.go (2)

176-260: Solid struct parsing; a few gaps to tighten.

  • time.Time format works for non-pointers only; pointer times won’t get date-time.
  • Arrays of structs (when not tagged nested) won’t carry item properties—might be intentional. For nested arrays, required fields on the element aren’t propagated to Items.Required.

If intentional that only validate:"nested" triggers deep parsing, ignore the second point. Otherwise, consider auto-nesting for struct fields.

To propagate Required for nested arrays, see parseNested diff below. Also update isTimeType to unwrap pointers (dedicated comment).


264-272: Optional: prefer lowerCamelCase fallback for JSON names.

Lowercasing the entire field (LocationIDlocationid) can be surprising. Consider converting PascalCase to lowerCamelCase when no json tag is present.

No diff included to avoid churn; happy to provide a helper if you want to adopt this.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a08a997 and 3815ff2.

📒 Files selected for processing (4)
  • parse.go (2 hunks)
  • parse_test.go (2 hunks)
  • type.go (1 hunks)
  • utils.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
parse.go (1)
type.go (2)
  • SchemaObject (81-90)
  • ItemsObject (97-103)
parse_test.go (1)
parse.go (1)
  • ParseSchema (177-260)

Comment thread parse_test.go
Comment on lines +144 to +147
text, err := json.Marshal(defintion)
require.Nil(t, err)
require.Equal(t, `{"type":"object","properties":{"category":{"type":"string","example":"paid-time-off"},"config":{"type":"object","properties":{"accrualPolicy":{"type":"object","properties":{"accrualMethod":{"type":"string","example":"year"},"accrualRates":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}}}},"allowedApplyFuture":{"type":"boolean","example":"true"},"annualResetPolicy":{"type":"object","properties":{"date":{"type":"string","example":"2024-01-01"},"type":{"type":"string","example":"calendarDate"}}},"autoApproval":{"type":"object","properties":{"expireDuration":{"type":"integer","example":"72"},"isEnable":{"type":"boolean","example":"true"},"leaveAmount":{"type":"number","example":"3"}}},"carryForwardPolicy":{"type":"object","properties":{"carryForwardRates":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}},"expireDuration":{"type":"integer","example":"90"}}},"emailReminder":{"type":"object","properties":{"expireDuration":{"type":"integer","example":"24"},"isEnable":{"type":"boolean","example":"true"}}},"leaveApplicationStart":{"type":"integer","example":"60"},"maxLeaveAmount":{"type":"number","example":"5"},"minLeaveAmount":{"type":"number","example":"0.5"},"newHireProbationPolicy":{"type":"object","properties":{"isEnable":{"type":"boolean","example":"false"},"rules":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}}}},"timeUnit":{"type":"string","example":"d"}}},"country":{"type":"string","example":"US"},"locationId":{"type":"string","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"name":{"type":"string","example":"Annual Leave"},"requiredInfo":{"type":"object","properties":{"employeeType":{"type":"string","example":"full-time"},"gender":{"type":"string","example":"male"}}}}}`, string(text))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid brittle JSON string equality; compare structurally.

Map key order in JSON is not guaranteed; exact string compare will be flaky. Compare decoded structures instead (or use JSONEq).

Apply this diff:

-	text, err := json.Marshal(defintion)
-	require.Nil(t, err)
-	require.Equal(t, `{"type":"object","properties":{"category":{"type":"string","example":"paid-time-off"},"config":{"type":"object","properties":{"accrualPolicy":{"type":"object","properties":{"accrualMethod":{"type":"string","example":"year"},"accrualRates":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}}}},"allowedApplyFuture":{"type":"boolean","example":"true"},"annualResetPolicy":{"type":"object","properties":{"date":{"type":"string","example":"2024-01-01"},"type":{"type":"string","example":"calendarDate"}}},"autoApproval":{"type":"object","properties":{"expireDuration":{"type":"integer","example":"72"},"isEnable":{"type":"boolean","example":"true"},"leaveAmount":{"type":"number","example":"3"}}},"carryForwardPolicy":{"type":"object","properties":{"carryForwardRates":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}},"expireDuration":{"type":"integer","example":"90"}}},"emailReminder":{"type":"object","properties":{"expireDuration":{"type":"integer","example":"24"},"isEnable":{"type":"boolean","example":"true"}}},"leaveApplicationStart":{"type":"integer","example":"60"},"maxLeaveAmount":{"type":"number","example":"5"},"minLeaveAmount":{"type":"number","example":"0.5"},"newHireProbationPolicy":{"type":"object","properties":{"isEnable":{"type":"boolean","example":"false"},"rules":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}}}},"timeUnit":{"type":"string","example":"d"}}},"country":{"type":"string","example":"US"},"locationId":{"type":"string","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"name":{"type":"string","example":"Annual Leave"},"requiredInfo":{"type":"object","properties":{"employeeType":{"type":"string","example":"full-time"},"gender":{"type":"string","example":"male"}}}}}`, string(text))
+	text, err := json.Marshal(defintion)
+	require.NoError(t, err)
+	var got, want map[string]any
+	require.NoError(t, json.Unmarshal(text, &got))
+	require.NoError(t, json.Unmarshal([]byte(`{"type":"object","properties":{"category":{"type":"string","example":"paid-time-off"},"config":{"type":"object","properties":{"accrualPolicy":{"type":"object","properties":{"accrualMethod":{"type":"string","example":"year"},"accrualRates":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}}}},"allowedApplyFuture":{"type":"boolean","example":"true"},"annualResetPolicy":{"type":"object","properties":{"date":{"type":"string","example":"2024-01-01"},"type":{"type":"string","example":"calendarDate"}}},"autoApproval":{"type":"object","properties":{"expireDuration":{"type":"integer","example":"72"},"isEnable":{"type":"boolean","example":"true"},"leaveAmount":{"type":"number","example":"3"}}},"carryForwardPolicy":{"type":"object","properties":{"carryForwardRates":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}},"expireDuration":{"type":"integer","example":"90"}}},"emailReminder":{"type":"object","properties":{"expireDuration":{"type":"integer","example":"24"},"isEnable":{"type":"boolean","example":"true"}}},"leaveApplicationStart":{"type":"integer","example":"60"},"maxLeaveAmount":{"type":"number","example":"5"},"minLeaveAmount":{"type":"number","example":"0.5"},"newHireProbationPolicy":{"type":"object","properties":{"isEnable":{"type":"boolean","example":"false"},"rules":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","example":"0"},"to":{"type":"integer","example":"100"},"value":{"type":"integer","example":"12"}}}}}},"timeUnit":{"type":"string","example":"d"}}},"country":{"type":"string","example":"US"},"locationId":{"type":"string","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"name":{"type":"string","example":"Annual Leave"},"requiredInfo":{"type":"object","properties":{"employeeType":{"type":"string","example":"full-time"},"gender":{"type":"string","example":"male"}}}}}`), &want))
+	require.Equal(t, want, got)
🤖 Prompt for AI Agents
In parse_test.go around lines 144 to 147, the test currently does a brittle
string equality check of JSON output; replace the exact string compare with a
structural JSON comparison — either use require.JSONEq(t, expectedJSONString,
string(text)) (preferred) or unmarshal both expected JSON and text into
interface{} and use require.Equal(t, expectedObj, actualObj) so key order won’t
break the test.

Comment thread parse.go
Comment on lines +274 to +276
func isTimeType(t reflect.Type) bool {
return t == reflect.TypeOf(time.Time{})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle pointer to time.Time.

isTimeType should unwrap pointers so *time.Time gets format: date-time.

-func isTimeType(t reflect.Type) bool {
-	return t == reflect.TypeOf(time.Time{})
-}
+func isTimeType(t reflect.Type) bool {
+	if t == nil {
+		return false
+	}
+	for t.Kind() == reflect.Ptr {
+		t = t.Elem()
+	}
+	return t == reflect.TypeOf(time.Time{})
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func isTimeType(t reflect.Type) bool {
return t == reflect.TypeOf(time.Time{})
}
func isTimeType(t reflect.Type) bool {
if t == nil {
return false
}
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t == reflect.TypeOf(time.Time{})
}
🤖 Prompt for AI Agents
In parse.go around lines 274 to 276, isTimeType only recognizes time.Time but
not *time.Time; update it to unwrap pointer types before comparison by looping
while t.Kind() == reflect.Ptr { t = t.Elem() } (ensuring t is non-nil) and then
compare against reflect.TypeOf(time.Time{}), so pointer-to-time values are
correctly detected as time types.

Comment thread parse.go
Comment on lines +279 to +326
// Handle pointer or slice types
switch t.Kind() {
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct {
return ParseSchema(reflect.New(t.Elem()).Interface())
}
case reflect.Slice, reflect.Array:
elemType := t.Elem()

switch elemType.Kind() {
case reflect.Struct:
// Array of struct
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: "object",
Properties: ParseSchema(reflect.New(elemType).Interface()).Properties,
},
}

case reflect.Ptr:
// Array of pointer to struct
if elemType.Elem().Kind() == reflect.Struct {
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: "object",
Properties: ParseSchema(reflect.New(elemType.Elem()).Interface()).Properties,
},
}
}
fallthrough

default:
// Array of primitive type (string, int, etc.)
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: mappingType(elemType),
},
}
}

case reflect.Struct:
return ParseSchema(reflect.New(t).Interface())
}
return &SchemaObject{Type: mappingType(t)}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle pointer-to-slice/array and propagate Required for array items.

  • parseNested doesn’t handle *[]T/[]*T; pointer case only covers struct.
  • Item-level Required is omitted for arrays of structs, despite ItemsObject.Required being available.

Apply this diff:

 func parseNested(v reflect.Value, t reflect.Type) *SchemaObject {
-	// Handle pointer or slice types
+	// Handle pointer or slice types
 	switch t.Kind() {
 	case reflect.Ptr:
-		if t.Elem().Kind() == reflect.Struct {
-			return ParseSchema(reflect.New(t.Elem()).Interface())
-		}
+		// Deref and re-enter to support *T, *[]T, []*T
+		return parseNested(v, t.Elem())
 	case reflect.Slice, reflect.Array:
 		elemType := t.Elem()
 
 		switch elemType.Kind() {
 		case reflect.Struct:
 			// Array of struct
-			return &SchemaObject{
+			inner := ParseSchema(reflect.New(elemType).Interface())
+			return &SchemaObject{
 				Type: "array",
 				Items: &ItemsObject{
-					Type:       "object",
-					Properties: ParseSchema(reflect.New(elemType).Interface()).Properties,
+					Type:       "object",
+					Properties: inner.Properties,
+					Required:   inner.Required,
 				},
 			}
 
 		case reflect.Ptr:
 			// Array of pointer to struct
 			if elemType.Elem().Kind() == reflect.Struct {
-				return &SchemaObject{
+				inner := ParseSchema(reflect.New(elemType.Elem()).Interface())
+				return &SchemaObject{
 					Type: "array",
 					Items: &ItemsObject{
-						Type:       "object",
-						Properties: ParseSchema(reflect.New(elemType.Elem()).Interface()).Properties,
+						Type:       "object",
+						Properties: inner.Properties,
+						Required:   inner.Required,
 					},
 				}
 			}
 			fallthrough
 
 		default:
 			// Array of primitive type (string, int, etc.)
 			return &SchemaObject{
 				Type: "array",
 				Items: &ItemsObject{
-					Type: mappingType(elemType),
+					Type: mappingType(elemType),
 				},
 			}
 		}
 
 	case reflect.Struct:
 		return ParseSchema(reflect.New(t).Interface())
 	}
 	return &SchemaObject{Type: mappingType(t)}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle pointer or slice types
switch t.Kind() {
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct {
return ParseSchema(reflect.New(t.Elem()).Interface())
}
case reflect.Slice, reflect.Array:
elemType := t.Elem()
switch elemType.Kind() {
case reflect.Struct:
// Array of struct
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: "object",
Properties: ParseSchema(reflect.New(elemType).Interface()).Properties,
},
}
case reflect.Ptr:
// Array of pointer to struct
if elemType.Elem().Kind() == reflect.Struct {
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: "object",
Properties: ParseSchema(reflect.New(elemType.Elem()).Interface()).Properties,
},
}
}
fallthrough
default:
// Array of primitive type (string, int, etc.)
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: mappingType(elemType),
},
}
}
case reflect.Struct:
return ParseSchema(reflect.New(t).Interface())
}
return &SchemaObject{Type: mappingType(t)}
}
// Handle pointer or slice types
switch t.Kind() {
case reflect.Ptr:
// Deref and re-enter to support *T, *[]T, []*T
return parseNested(v, t.Elem())
case reflect.Slice, reflect.Array:
elemType := t.Elem()
switch elemType.Kind() {
case reflect.Struct:
// Array of struct
inner := ParseSchema(reflect.New(elemType).Interface())
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: "object",
Properties: inner.Properties,
Required: inner.Required,
},
}
case reflect.Ptr:
// Array of pointer to struct
if elemType.Elem().Kind() == reflect.Struct {
inner := ParseSchema(reflect.New(elemType.Elem()).Interface())
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: "object",
Properties: inner.Properties,
Required: inner.Required,
},
}
}
fallthrough
default:
// Array of primitive type (string, int, etc.)
return &SchemaObject{
Type: "array",
Items: &ItemsObject{
Type: mappingType(elemType),
},
}
}
case reflect.Struct:
return ParseSchema(reflect.New(t).Interface())
}
return &SchemaObject{Type: mappingType(t)}
}
🤖 Prompt for AI Agents
In parse.go around lines 279–326, update parseNested to handle
pointer-to-slice/array (e.g., *[]T and *[]*T) and to copy item-level Required
fields when array items are objects: when t.Kind() == reflect.Ptr and
t.Elem().Kind() is Slice or Array, descend into t.Elem() (i.e., treat
pointer-to-slice as slice) and process the element type the same as the
Slice/Array case; in the Slice/Array branch, when building Items for element
kinds Struct or Ptr->Struct, call ParseSchema on the element type, assign the
returned Properties to Items.Properties and also copy its Required slice into
Items.Required; keep the existing fallback for primitive element types using
mappingType. Ensure nil checks before accessing Elem() kinds.

@ren0503 ren0503 merged commit ae163d3 into master Oct 26, 2025
4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Nov 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parse Schema For Complex Struct

2 participants