@@ -164,15 +164,16 @@ func validateOperationParameters(resource *ResourceInfo, primaryID string, schem
164164 }
165165 } else if isEntityID (param ) {
166166 // This is another ID-like parameter without manual mapping
167- // Check if it maps to a field in the entity (not the primary id field)
168- if checkFieldExistsInEntityWithRefResolution (param , entityProps , schemas ) {
169- // This parameter maps to a real entity field - it's valid
170- continue
171- } else {
172- // This ID parameter doesn't map to any entity field
167+ // Only consider it problematic if it conflicts with the primary ID or claims to be this entity's ID
168+ if param != primaryID && mapsToEntityID (param , resource .EntityName ) {
169+ // This parameter claims to be this entity's ID but isn't the primary ID - that's a conflict
173170 hasConflictingEntityIDs = true
174171 break
175172 }
173+
174+ // For other ID parameters (like team_id, parent_id, etc.), they're just foreign keys
175+ // and don't need to exist in the entity schema - they're fine
176+ // we weren't able to map them but they don't conflict with the primary ID
176177 }
177178 // Non-ID parameters are always OK
178179 }
@@ -276,42 +277,48 @@ func checkFieldExistsInEntityWithRefResolution(fieldPath string, entityProps map
276277 currentLevel := entityProps
277278
278279 for i , part := range parts {
280+
279281 if prop , exists := currentLevel [part ]; exists {
282+
280283 if i == len (parts )- 1 {
281284 return true
282285 }
283286
284287 if propMap , ok := prop .(map [string ]interface {}); ok {
288+ // Check if it has direct properties
285289 if nestedProps , hasProps := propMap ["properties" ].(map [string ]interface {}); hasProps {
286290 currentLevel = nestedProps
287291 continue
288292 }
289293
294+ // Check if it has a $ref that needs resolution
290295 if ref , hasRef := propMap ["$ref" ].(string ); hasRef {
291- if strings .HasPrefix (ref , "#/components/schemas/" ) {
292- schemaName := strings .TrimPrefix (ref , "#/components/schemas/" )
293-
294- if referencedSchema , exists := schemas [schemaName ]; exists {
295- specData , _ := json .Marshal (referencedSchema )
296- var schemaMap map [string ]interface {}
297- json .Unmarshal (specData , & schemaMap )
298-
299- if refProps , hasRefProps := schemaMap ["properties" ].(map [string ]interface {}); hasRefProps {
300- currentLevel = refProps
301- continue
302- }
296+
297+ resolvedSchema := resolveSchemaRef (ref , schemas )
298+ if resolvedSchema != nil {
299+
300+ if refProps , hasRefProps := resolvedSchema ["properties" ].(map [string ]interface {}); hasRefProps {
301+ currentLevel = refProps
302+ continue
303+ } else {
304+ fmt .Printf (" Debug: Resolved schema has no properties\n " )
303305 }
306+ } else {
307+ fmt .Printf (" Debug: Failed to resolve $ref '%s'\n " , ref )
304308 }
305309
306310 fmt .Printf (" Warning: Cannot resolve $ref for nested property validation: %s (ref: %s)\n " , fieldPath , ref )
307- return false // We don't want to assume the ref is valid if we can't resolve it
311+ return false
308312 }
309313 }
310314
311315 // If we can't navigate deeper but haven't reached the end, the path is invalid
316+ fmt .Printf (" Debug: Cannot navigate deeper from part '%s' - not a valid object structure\n " , part )
312317 fmt .Printf (" Cannot navigate to nested property: %s at part: %s\n " , fieldPath , part )
313318 return false
314319 } else {
320+ fmt .Printf (" Debug: Part '%s' not found in current level\n " , part )
321+ fmt .Printf (" Debug: Available keys in current level: %v\n " , getKeys (currentLevel ))
315322 fmt .Printf (" Field does not exist: %s at part: %s\n " , fieldPath , part )
316323 return false
317324 }
@@ -320,6 +327,55 @@ func checkFieldExistsInEntityWithRefResolution(fieldPath string, entityProps map
320327 return false
321328}
322329
330+ func getKeys (m map [string ]interface {}) []string {
331+ keys := make ([]string , 0 , len (m ))
332+ for k := range m {
333+ keys = append (keys , k )
334+ }
335+ return keys
336+ }
337+
338+ func resolveSchemaRef (ref string , schemas map [string ]interface {}) map [string ]interface {} {
339+ if ! strings .HasPrefix (ref , "#/components/schemas/" ) {
340+ return nil
341+ }
342+
343+ schemaName := strings .TrimPrefix (ref , "#/components/schemas/" )
344+
345+ if referencedSchema , exists := schemas [schemaName ]; exists {
346+ specData , _ := json .Marshal (referencedSchema )
347+ var schemaMap map [string ]interface {}
348+ json .Unmarshal (specData , & schemaMap )
349+
350+ // At time of writing we have validation rules which prevent oneOfs in our swagger generation within laddertruck.
351+ // So we only have allOf patterns to work with, these are generally created by our NullableWrappers
352+ // NullableWrappers are used to allow nullable properties in the API (also created in laddertruck), so we need to resolve them
353+ if allOf , hasAllOf := schemaMap ["allOf" ].([]interface {}); hasAllOf {
354+ for _ , item := range allOf {
355+ if itemMap , ok := item .(map [string ]interface {}); ok {
356+ if innerRef , hasInnerRef := itemMap ["$ref" ].(string ); hasInnerRef {
357+ resolved := resolveSchemaRef (innerRef , schemas )
358+ if resolved != nil {
359+ return resolved
360+ }
361+ }
362+ if _ , hasProps := itemMap ["properties" ].(map [string ]interface {}); hasProps {
363+ return itemMap
364+ }
365+ }
366+ }
367+ }
368+
369+ if _ , hasProps := schemaMap ["properties" ].(map [string ]interface {}); hasProps {
370+ return schemaMap
371+ }
372+
373+ return schemaMap
374+ }
375+
376+ return nil
377+ }
378+
323379func identifyEntityPrimaryID (resource * ResourceInfo , schemas map [string ]interface {}) (string , bool ) {
324380 allParams := make (map [string ]bool )
325381
@@ -380,14 +436,26 @@ func identifyEntityPrimaryID(resource *ResourceInfo, schemas map[string]interfac
380436 }
381437 }
382438 }
383- }
384439
385- if allParams ["id" ] {
386- return "id" , true
440+ if allParams ["id" ] {
441+ return "id" , true
442+ }
387443 }
388444
389445 if len (allParams ) == 1 && (hasID || hasSlug ) {
390446 for param := range allParams {
447+ if ! strings .Contains (param , "team_id" ) && ! strings .Contains (param , "parent_id" ) {
448+ return param , true
449+ }
450+ }
451+ }
452+
453+ // For complex cases with multiple parameters, try to identify the most likely primary ID
454+ // Look for parameters that end with the entity name or are simple "id"
455+ entityBase := strings .ToLower (strings .TrimSuffix (resource .EntityName , "Entity" ))
456+ for param := range allParams {
457+ lowerParam := strings .ToLower (param )
458+ if lowerParam == entityBase + "_id" || lowerParam == "id" {
391459 return param , true
392460 }
393461 }
0 commit comments