Skip to main content

openapi_to_rust/
analysis.rs

1use crate::openapi::{Discriminator, OpenApiSpec, Schema, SchemaType as OpenApiSchemaType};
2use crate::{GeneratorError, Result};
3use serde_json::Value;
4use std::collections::{BTreeMap, HashSet};
5use std::path::Path;
6
7#[derive(Debug, Clone)]
8pub struct SchemaAnalysis {
9    /// All schemas indexed by name
10    pub schemas: BTreeMap<String, AnalyzedSchema>,
11    /// Dependency graph for generation ordering
12    pub dependencies: DependencyGraph,
13    /// Detected patterns and transformations
14    pub patterns: DetectedPatterns,
15    /// OpenAPI operations and their request/response schemas
16    pub operations: BTreeMap<String, OperationInfo>,
17}
18
19#[derive(Debug, Clone)]
20pub struct AnalyzedSchema {
21    pub name: String,
22    pub original: Value,
23    pub schema_type: SchemaType,
24    pub dependencies: HashSet<String>,
25    pub nullable: bool,
26    pub description: Option<String>,
27    pub default: Option<serde_json::Value>,
28}
29
30#[derive(Debug, Clone)]
31pub enum SchemaType {
32    /// Simple primitive type
33    Primitive { rust_type: String },
34    /// Object with properties
35    Object {
36        properties: BTreeMap<String, PropertyInfo>,
37        required: HashSet<String>,
38        additional_properties: bool,
39    },
40    /// Discriminated union (oneOf + discriminator)
41    DiscriminatedUnion {
42        discriminator_field: String,
43        variants: Vec<UnionVariant>,
44    },
45    /// Simple union (anyOf without discriminator)
46    Union { variants: Vec<SchemaRef> },
47    /// Array type
48    Array { item_type: Box<SchemaType> },
49    /// String enum
50    StringEnum { values: Vec<String> },
51    /// Extensible enum with known values and custom variant
52    ExtensibleEnum { known_values: Vec<String> },
53    /// Schema composition (allOf)
54    Composition { schemas: Vec<SchemaRef> },
55    /// Reference to another schema
56    Reference { target: String },
57}
58
59#[derive(Debug, Clone)]
60pub struct PropertyInfo {
61    pub schema_type: SchemaType,
62    pub nullable: bool,
63    pub description: Option<String>,
64    pub default: Option<serde_json::Value>,
65    pub serde_attrs: Vec<String>,
66}
67
68#[derive(Debug, Clone)]
69pub struct UnionVariant {
70    pub rust_name: String,
71    pub type_name: String,
72    pub discriminator_value: String,
73    pub schema_ref: String,
74}
75
76#[derive(Debug, Clone)]
77pub struct SchemaRef {
78    pub target: String,
79    pub nullable: bool,
80}
81
82#[derive(Debug, Clone)]
83pub struct DependencyGraph {
84    pub edges: BTreeMap<String, HashSet<String>>,
85    /// Set of schemas that have recursive dependencies
86    pub recursive_schemas: HashSet<String>,
87}
88
89#[derive(Debug, Clone)]
90pub struct DetectedPatterns {
91    /// Schemas that should use tagged enums (discriminated unions)
92    pub tagged_enum_schemas: HashSet<String>,
93    /// Schemas that should use untagged enums (simple unions)  
94    pub untagged_enum_schemas: HashSet<String>,
95    /// Auto-detected type mappings for discriminated unions
96    pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
97}
98
99/// Information about an OpenAPI operation
100#[derive(Debug, Clone, serde::Serialize)]
101pub struct OperationInfo {
102    /// Operation ID
103    pub operation_id: String,
104    /// HTTP method (GET, POST, etc.)
105    pub method: String,
106    /// Path template
107    pub path: String,
108    /// Request body content type and schema (if any)
109    pub request_body: Option<RequestBodyContent>,
110    /// Response schemas by status code
111    pub response_schemas: BTreeMap<String, String>,
112    /// Parameters (path, query, header)
113    pub parameters: Vec<ParameterInfo>,
114    /// Whether this operation supports streaming
115    pub supports_streaming: bool,
116    /// Stream parameter name if applicable
117    pub stream_parameter: Option<String>,
118}
119
120/// Content type and schema for a request body
121#[derive(Debug, Clone, serde::Serialize)]
122#[serde(tag = "kind")]
123pub enum RequestBodyContent {
124    Json { schema_name: String },
125    FormUrlEncoded { schema_name: String },
126    Multipart,
127    OctetStream,
128    TextPlain,
129}
130
131impl RequestBodyContent {
132    /// Get the schema name if this content type has one
133    pub fn schema_name(&self) -> Option<&str> {
134        match self {
135            Self::Json { schema_name } | Self::FormUrlEncoded { schema_name } => Some(schema_name),
136            _ => None,
137        }
138    }
139}
140
141/// Information about an operation parameter
142#[derive(Debug, Clone, serde::Serialize)]
143pub struct ParameterInfo {
144    /// Parameter name
145    pub name: String,
146    /// Parameter location (path, query, header, cookie)
147    pub location: String,
148    /// Whether the parameter is required
149    pub required: bool,
150    /// Schema reference for the parameter type
151    pub schema_ref: Option<String>,
152    /// Rust type for this parameter
153    pub rust_type: String,
154}
155
156impl Default for DependencyGraph {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl DependencyGraph {
163    pub fn new() -> Self {
164        Self {
165            edges: BTreeMap::new(),
166            recursive_schemas: HashSet::new(),
167        }
168    }
169
170    pub fn add_dependency(&mut self, from: String, to: String) {
171        self.edges.entry(from).or_default().insert(to);
172    }
173
174    /// Get topological sort order for generation
175    pub fn topological_sort(&mut self) -> Result<Vec<String>> {
176        // First, detect and handle recursive dependencies
177        self.detect_recursive_schemas();
178
179        // Create a temporary graph without self-referencing edges for sorting
180        let mut temp_edges = self.edges.clone();
181        for (schema, deps) in &mut temp_edges {
182            deps.remove(schema); // Remove self-references
183        }
184
185        let mut visited = HashSet::new();
186        let mut temp_visited = HashSet::new();
187        let mut result = Vec::new();
188
189        // Visit all nodes using the temporary graph in sorted order for deterministic output
190        let mut all_nodes: Vec<_> = temp_edges.keys().collect();
191        all_nodes.sort();
192        for node in all_nodes {
193            if !visited.contains(node) {
194                self.visit_node_recursive(
195                    node,
196                    &temp_edges,
197                    &mut visited,
198                    &mut temp_visited,
199                    &mut result,
200                )?;
201            }
202        }
203
204        result.reverse();
205        Ok(result)
206    }
207
208    fn detect_recursive_schemas(&mut self) {
209        for (schema, deps) in &self.edges {
210            if deps.contains(schema) {
211                // Direct self-reference
212                self.recursive_schemas.insert(schema.clone());
213            } else {
214                // Check for indirect cycles
215                if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
216                    self.recursive_schemas.insert(schema.clone());
217                }
218            }
219        }
220
221        // Also detect mutual recursion (like GraphNode <-> GraphEdge)
222        for (schema, deps) in &self.edges {
223            for dep in deps {
224                if let Some(dep_deps) = self.edges.get(dep) {
225                    if dep_deps.contains(schema) {
226                        // Mutual recursion detected
227                        self.recursive_schemas.insert(schema.clone());
228                        self.recursive_schemas.insert(dep.clone());
229                    }
230                }
231            }
232        }
233    }
234
235    fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
236        if visited.contains(current) {
237            return false; // Already checked this path
238        }
239
240        visited.insert(current.to_string());
241
242        if let Some(deps) = self.edges.get(current) {
243            for dep in deps {
244                if dep == start {
245                    return true; // Found cycle back to start
246                }
247                if self.has_cycle_from(start, dep, visited) {
248                    return true;
249                }
250            }
251        }
252
253        false
254    }
255
256    #[allow(clippy::only_used_in_recursion)]
257    fn visit_node_recursive(
258        &self,
259        node: &str,
260        temp_edges: &BTreeMap<String, HashSet<String>>,
261        visited: &mut HashSet<String>,
262        temp_visited: &mut HashSet<String>,
263        result: &mut Vec<String>,
264    ) -> Result<()> {
265        if temp_visited.contains(node) {
266            // This should not happen with cycle-free temp graph, but just in case
267            return Ok(());
268        }
269
270        if visited.contains(node) {
271            return Ok(());
272        }
273
274        temp_visited.insert(node.to_string());
275
276        if let Some(dependencies) = temp_edges.get(node) {
277            // Sort dependencies for deterministic topological order
278            let mut sorted_deps: Vec<_> = dependencies.iter().collect();
279            sorted_deps.sort();
280            for dep in sorted_deps {
281                self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
282            }
283        }
284
285        temp_visited.remove(node);
286        visited.insert(node.to_string());
287        result.push(node.to_string());
288
289        Ok(())
290    }
291}
292
293/// Merge schema extension files into the main OpenAPI specification
294/// Uses simple recursive JSON object merging
295pub fn merge_schema_extensions(
296    main_spec: Value,
297    extension_paths: &[impl AsRef<Path>],
298) -> Result<Value> {
299    let mut result = main_spec;
300
301    for path in extension_paths {
302        let extension = load_extension_file(path.as_ref())?;
303        result = merge_json_objects_with_replacements(result, extension)?;
304    }
305
306    Ok(result)
307}
308
309/// Load an extension file and parse as JSON
310fn load_extension_file(path: &Path) -> Result<Value> {
311    let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
312        message: format!("Failed to read file {}: {}", path.display(), e),
313    })?;
314
315    serde_json::from_str(&content).map_err(GeneratorError::ParseError)
316}
317
318/// Merge JSON objects with explicit replacement support
319fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
320    // Extract replacement rules from the extension
321    let replacements = extract_replacement_rules(&extension);
322
323    // Perform the merge with replacement awareness
324    Ok(merge_json_objects_with_rules(
325        main,
326        extension,
327        &replacements,
328    ))
329}
330
331/// Extract x-replacements rules from extension
332fn extract_replacement_rules(
333    extension: &Value,
334) -> std::collections::HashMap<String, (String, String)> {
335    let mut rules = std::collections::HashMap::new();
336
337    if let Some(x_replacements) = extension.get("x-replacements") {
338        if let Some(x_replacements_obj) = x_replacements.as_object() {
339            for (schema_name, replacement_rule) in x_replacements_obj {
340                if let Some(rule_obj) = replacement_rule.as_object() {
341                    if let (Some(replace), Some(with)) = (
342                        rule_obj.get("replace").and_then(|v| v.as_str()),
343                        rule_obj.get("with").and_then(|v| v.as_str()),
344                    ) {
345                        rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
346                        // println!("📋 Replacement rule: In {}, replace {} with {}", schema_name, replace, with);
347                    }
348                }
349            }
350        }
351    }
352
353    rules
354}
355
356/// Check if a variant should be replaced based on explicit replacement rules
357fn should_replace_variant(
358    schema_name: &str,
359    extension_refs: &[String],
360    replacements: &std::collections::HashMap<String, (String, String)>,
361) -> bool {
362    // Check all replacement rules
363    for (replace_schema, with_schema) in replacements.values() {
364        if schema_name == replace_schema {
365            // This schema should be replaced - check if the replacement schema is in extensions
366            let replacement_exists = extension_refs.iter().any(|ext_ref| {
367                let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
368                ext_schema_name == with_schema
369            });
370
371            if replacement_exists {
372                return true;
373            }
374        }
375    }
376
377    // Fallback to exact name match for complete replacement
378    extension_refs.iter().any(|ext_ref| {
379        let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
380        schema_name == ext_schema_name
381    })
382}
383
384/// Recursively merge two JSON values with replacement rules
385/// Objects are merged by combining properties
386/// Arrays are merged by concatenating
387/// Primitives in the extension override the main value
388fn merge_json_objects_with_rules(
389    main: Value,
390    extension: Value,
391    replacements: &std::collections::HashMap<String, (String, String)>,
392) -> Value {
393    match (main, extension) {
394        // Both objects - merge properties
395        (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
396            // Special handling for schema objects with oneOf/anyOf variants
397            if let (Some(main_variants), Some(ext_variants)) = (
398                extract_schema_variants(&Value::Object(main_obj.clone())),
399                extract_schema_variants(&Value::Object(ext_obj.clone())),
400            ) {
401                println!(
402                    "🔍 Merging union schemas: {} main variants, {} extension variants",
403                    main_variants.len(),
404                    ext_variants.len()
405                );
406                // Merge the variant arrays and use oneOf as the canonical key
407                // First, collect main variants, but filter out any that will be replaced by extension
408                let mut merged_variants = Vec::new();
409                let extension_refs: Vec<String> = ext_variants
410                    .iter()
411                    .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
412                    .map(|s| s.to_string())
413                    .collect();
414
415                // Add main variants that aren't being replaced
416                for main_variant in main_variants {
417                    if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
418                        // Check if this main variant should be replaced by an extension variant
419                        let schema_name = main_ref.split('/').next_back().unwrap_or("");
420                        let should_replace =
421                            should_replace_variant(schema_name, &extension_refs, replacements);
422
423                        if should_replace {
424                            println!("🔄 REPLACING {} (explicit rule)", schema_name);
425                        }
426
427                        if !should_replace {
428                            merged_variants.push(main_variant);
429                        }
430                    } else {
431                        // Keep non-ref variants
432                        merged_variants.push(main_variant);
433                    }
434                }
435
436                // Add all extension variants
437                for ext_variant in ext_variants {
438                    merged_variants.push(ext_variant);
439                }
440
441                // Remove old oneOf/anyOf keys and add the merged oneOf
442                main_obj.remove("oneOf");
443                main_obj.remove("anyOf");
444                main_obj.insert("oneOf".to_string(), Value::Array(merged_variants));
445
446                // Merge other properties normally
447                for (key, ext_value) in ext_obj {
448                    if key != "oneOf" && key != "anyOf" {
449                        match main_obj.get(&key) {
450                            Some(main_value) => {
451                                let merged_value = merge_json_objects_with_rules(
452                                    main_value.clone(),
453                                    ext_value,
454                                    replacements,
455                                );
456                                main_obj.insert(key, merged_value);
457                            }
458                            None => {
459                                main_obj.insert(key, ext_value);
460                            }
461                        }
462                    }
463                }
464
465                return Value::Object(main_obj);
466            }
467
468            // Normal object merging
469            for (key, ext_value) in ext_obj {
470                match main_obj.get(&key) {
471                    Some(main_value) => {
472                        // Key exists in both - recursively merge
473                        let merged_value = merge_json_objects_with_rules(
474                            main_value.clone(),
475                            ext_value,
476                            replacements,
477                        );
478                        main_obj.insert(key, merged_value);
479                    }
480                    None => {
481                        // Key only in extension - add it
482                        main_obj.insert(key, ext_value);
483                    }
484                }
485            }
486            Value::Object(main_obj)
487        }
488
489        // Both arrays - concatenate
490        (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
491            main_arr.extend(ext_arr);
492            Value::Array(main_arr)
493        }
494
495        // Extension overrides main for all other cases
496        (_, extension) => extension,
497    }
498}
499
500/// Extract schema variants from oneOf or anyOf properties
501fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
502    if let Value::Object(map) = obj {
503        if let Some(Value::Array(variants)) = map.get("oneOf") {
504            return Some(variants.clone());
505        }
506        if let Some(Value::Array(variants)) = map.get("anyOf") {
507            return Some(variants.clone());
508        }
509    }
510    None
511}
512
513pub struct SchemaAnalyzer {
514    schemas: BTreeMap<String, Schema>,
515    resolved_cache: BTreeMap<String, AnalyzedSchema>,
516    openapi_spec: Value,
517    current_schema_name: Option<String>,
518}
519
520impl SchemaAnalyzer {
521    pub fn new(openapi_spec: Value) -> Result<Self> {
522        let spec: OpenApiSpec =
523            serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
524        let schemas = Self::extract_schemas(&spec)?;
525
526        Ok(Self {
527            schemas,
528            resolved_cache: BTreeMap::new(),
529            openapi_spec,
530            current_schema_name: None,
531        })
532    }
533
534    /// Create a new analyzer with schema extensions merged in
535    pub fn new_with_extensions(
536        openapi_spec: Value,
537        extension_paths: &[std::path::PathBuf],
538    ) -> Result<Self> {
539        let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
540        Self::new(merged_spec)
541    }
542
543    /// Generate a context-aware name for inline types, arrays, and variants
544    /// This provides better naming than generic names like UnionArray1, InlineVariant2, etc.
545    fn generate_context_aware_name(
546        &self,
547        base_context: &str,
548        type_hint: &str,
549        index: usize,
550        schema: Option<&Schema>,
551    ) -> String {
552        // First, try to infer a better name from the schema structure
553        if let Some(schema) = schema {
554            // For arrays, check if we can derive name from items
555            if type_hint == "Array"
556                && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
557            {
558                if let Some(items_schema) = &schema.details().items {
559                    // Check for specific item types
560                    if let Some(item_type) = items_schema.schema_type() {
561                        match item_type {
562                            OpenApiSchemaType::Object => {
563                                return format!("{base_context}ItemArray");
564                            }
565                            OpenApiSchemaType::String => {
566                                return format!("{base_context}StringArray");
567                            }
568                            _ => {}
569                        }
570                    }
571                }
572            }
573        }
574
575        // Generate context-aware name based on type hint
576        match type_hint {
577            "Array" => {
578                // For arrays, always use context name instead of generic numbering
579                format!("{base_context}Array")
580            }
581            "Variant" | "InlineVariant" => {
582                // For variants, include index only if > 0 to keep first variant clean
583                if index == 0 {
584                    format!("{base_context}{type_hint}")
585                } else {
586                    format!("{}{}{}", base_context, type_hint, index + 1)
587                }
588            }
589            _ => {
590                // Default case
591                format!("{base_context}{type_hint}{index}")
592            }
593        }
594    }
595
596    /// Convert a string to PascalCase, handling underscores and hyphens
597    fn to_pascal_case(&self, s: &str) -> String {
598        s.split(['_', '-'])
599            .filter(|part| !part.is_empty())
600            .map(|part| {
601                let mut chars = part.chars();
602                match chars.next() {
603                    None => String::new(),
604                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
605                }
606            })
607            .collect()
608    }
609
610    fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
611        let schemas = spec
612            .components
613            .as_ref()
614            .and_then(|c| c.schemas.as_ref())
615            .ok_or_else(|| {
616                GeneratorError::InvalidSchema("No schemas found in OpenAPI spec".to_string())
617            })?;
618
619        // Convert BTreeMap to BTreeMap for deterministic iteration order
620        Ok(schemas
621            .iter()
622            .map(|(k, v)| (k.clone(), v.clone()))
623            .collect())
624    }
625
626    pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
627        let mut analysis = SchemaAnalysis {
628            schemas: BTreeMap::new(),
629            dependencies: DependencyGraph::new(),
630            patterns: DetectedPatterns {
631                tagged_enum_schemas: HashSet::new(),
632                untagged_enum_schemas: HashSet::new(),
633                type_mappings: BTreeMap::new(),
634            },
635            operations: BTreeMap::new(),
636        };
637
638        // First pass: detect patterns
639        self.detect_patterns(&mut analysis.patterns)?;
640
641        // Second pass: analyze each schema
642        let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
643        for schema_name in schema_names {
644            let analyzed = self.analyze_schema(&schema_name)?;
645
646            // Build dependency graph
647            for dep in &analyzed.dependencies {
648                analysis
649                    .dependencies
650                    .add_dependency(schema_name.clone(), dep.clone());
651            }
652
653            analysis.schemas.insert(schema_name, analyzed);
654        }
655
656        // Third pass: include any inline schemas that were generated during analysis
657        // BTreeMap maintains sorted order, so iteration is deterministic
658        for (inline_name, inline_schema) in &self.resolved_cache {
659            if !analysis.schemas.contains_key(inline_name) {
660                // Add the inline schema first
661                analysis
662                    .schemas
663                    .insert(inline_name.clone(), inline_schema.clone());
664
665                // Build dependency graph for inline schema's own dependencies
666                for dep in &inline_schema.dependencies {
667                    analysis
668                        .dependencies
669                        .add_dependency(inline_name.clone(), dep.clone());
670                }
671
672                // Check if any existing schemas depend on this inline schema
673                // We need to check ALL schemas, not just the ones already in analysis.schemas,
674                // because parent schemas might have been analyzed but their dependencies
675                // on inline schemas might not have been added to the dependency graph yet
676                let mut schemas_to_update = Vec::new();
677                for (schema_name, schema) in &analysis.schemas {
678                    // Skip self-reference
679                    if schema_name == inline_name {
680                        continue;
681                    }
682
683                    if schema.dependencies.contains(inline_name) {
684                        // The parent schema depends on this inline schema
685                        schemas_to_update.push(schema_name.clone());
686                    }
687                }
688
689                // Add the dependencies to the graph
690                for schema_name in schemas_to_update {
691                    analysis
692                        .dependencies
693                        .add_dependency(schema_name, inline_name.clone());
694                }
695            }
696        }
697
698        // Fourth pass: analyze OpenAPI operations
699        self.analyze_operations(&mut analysis)?;
700
701        // Fifth pass: include any inline schemas generated during operation analysis
702        // (e.g., inline response types)
703        for (inline_name, inline_schema) in &self.resolved_cache {
704            if !analysis.schemas.contains_key(inline_name) {
705                analysis
706                    .schemas
707                    .insert(inline_name.clone(), inline_schema.clone());
708
709                // Build dependency graph for inline schema's dependencies
710                for dep in &inline_schema.dependencies {
711                    analysis
712                        .dependencies
713                        .add_dependency(inline_name.clone(), dep.clone());
714                }
715            }
716        }
717
718        Ok(analysis)
719    }
720
721    fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
722        for (schema_name, schema) in &self.schemas {
723            // Detect discriminated unions
724            if self.is_discriminated_union(schema) {
725                patterns.tagged_enum_schemas.insert(schema_name.clone());
726
727                // Extract type mappings for this union
728                if let Some(mappings) = self.extract_type_mappings(schema)? {
729                    patterns.type_mappings.insert(schema_name.clone(), mappings);
730                }
731            }
732            // Detect simple unions
733            else if self.is_simple_union(schema) {
734                patterns.untagged_enum_schemas.insert(schema_name.clone());
735            }
736        }
737
738        Ok(())
739    }
740
741    fn is_discriminated_union(&self, schema: &Schema) -> bool {
742        // Check for explicit discriminator
743        if schema.is_discriminated_union() {
744            return true;
745        }
746
747        // Auto-detect from union patterns with any common const field
748        if let Some(variants) = schema.union_variants() {
749            return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
750        }
751
752        false
753    }
754
755    fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
756        variants.iter().all(|variant| {
757            if let Some(ref_str) = variant.reference() {
758                // $ref variant: resolve and check the referenced schema
759                if let Some(schema_name) = self.extract_schema_name(ref_str) {
760                    if let Some(schema) = self.schemas.get(schema_name) {
761                        return self.has_const_discriminator_field(schema, field_name);
762                    }
763                }
764            } else {
765                // Inline variant: check properties directly
766                return self.has_const_discriminator_field(variant, field_name);
767            }
768            false
769        })
770    }
771
772    /// Scan all variants to find any common property that has a const/single-enum value
773    /// across all variants. Returns the field name if found.
774    /// Prioritizes "type" if it matches (most common convention).
775    fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
776        if variants.is_empty() {
777            return None;
778        }
779
780        // Collect candidate field names from the first variant
781        let first_variant = &variants[0];
782        let first_schema = if let Some(ref_str) = first_variant.reference() {
783            let schema_name = self.extract_schema_name(ref_str)?;
784            self.schemas.get(schema_name)?
785        } else {
786            first_variant
787        };
788
789        let properties = first_schema.details().properties.as_ref()?;
790        let mut candidates: Vec<String> = Vec::new();
791
792        for (field_name, field_schema) in properties {
793            let details = field_schema.details();
794            let is_const = details.const_value.is_some()
795                || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
796                || details.extra.contains_key("const");
797            if is_const {
798                candidates.push(field_name.clone());
799            }
800        }
801
802        if candidates.is_empty() {
803            return None;
804        }
805
806        // Prioritize "type" if it's among candidates
807        candidates.sort_by(|a, b| {
808            if a == "type" {
809                std::cmp::Ordering::Less
810            } else if b == "type" {
811                std::cmp::Ordering::Greater
812            } else {
813                a.cmp(b)
814            }
815        });
816
817        // Check each candidate against all variants
818        for candidate in &candidates {
819            if self.all_variants_have_const_field(variants, candidate) {
820                return Some(candidate.clone());
821            }
822        }
823
824        None
825    }
826
827    fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
828        if let Some(properties) = &schema.details().properties {
829            if let Some(field) = properties.get(field_name) {
830                // Check for const value (OpenAPI 3.1 style)
831                if field.details().const_value.is_some() {
832                    return true;
833                }
834                // Check if it's an enum field with a single value
835                if let Some(enum_vals) = &field.details().enum_values {
836                    return enum_vals.len() == 1;
837                }
838                // Fallback: check extra fields for const
839                return field.details().extra.contains_key("const");
840            }
841        }
842        false
843    }
844
845    fn is_simple_union(&self, schema: &Schema) -> bool {
846        if let Some(variants) = schema.union_variants() {
847            // Simple union: multiple types but not nullable pattern
848            if variants.len() > 1 && !schema.is_nullable_pattern() {
849                let has_refs = variants.iter().any(|v| v.is_reference());
850                return has_refs;
851            }
852        }
853        false
854    }
855
856    fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
857        let variants = schema.union_variants().ok_or_else(|| {
858            GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
859        })?;
860
861        // Get the discriminator field name from the schema
862        let discriminator_field = if let Some(discriminator) = schema.discriminator() {
863            discriminator.property_name.clone()
864        } else if let Some(detected) = self.detect_discriminator_field(variants) {
865            detected
866        } else {
867            "type".to_string() // fallback to "type" for auto-detected discriminated unions
868        };
869
870        let mut mappings = BTreeMap::new();
871
872        for variant in variants {
873            if let Some(ref_str) = variant.reference() {
874                if let Some(type_name) = self.extract_schema_name(ref_str) {
875                    if let Some(variant_schema) = self.schemas.get(type_name) {
876                        if let Some(discriminator_value) = self
877                            .extract_discriminator_value_for_field(
878                                variant_schema,
879                                &discriminator_field,
880                            )
881                        {
882                            mappings.insert(type_name.to_string(), discriminator_value);
883                        }
884                    }
885                }
886            }
887        }
888
889        if mappings.is_empty() {
890            Ok(None)
891        } else {
892            Ok(Some(mappings))
893        }
894    }
895
896    #[allow(dead_code)]
897    fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
898        self.extract_discriminator_value_for_field(schema, "type")
899    }
900
901    fn extract_discriminator_value_for_field(
902        &self,
903        schema: &Schema,
904        field_name: &str,
905    ) -> Option<String> {
906        if let Some(properties) = &schema.details().properties {
907            if let Some(type_field) = properties.get(field_name) {
908                // Check for const value first (highest priority)
909                if let Some(const_value) = &type_field.details().const_value {
910                    if let Some(value) = const_value.as_str() {
911                        return Some(value.to_string());
912                    }
913                }
914                // Check for enum with single value
915                if let Some(enum_values) = &type_field.details().enum_values {
916                    if enum_values.len() == 1 {
917                        return enum_values[0].as_str().map(|s| s.to_string());
918                    }
919                }
920                // Check for const value in extra fields
921                if let Some(const_value) = type_field.details().extra.get("const") {
922                    return const_value.as_str().map(|s| s.to_string());
923                }
924                // Check for x-stainless-const with default value
925                if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
926                    if stainless_const.as_bool() == Some(true) {
927                        if let Some(default_value) = &type_field.details().default {
928                            if let Some(value) = default_value.as_str() {
929                                return Some(value.to_string());
930                            }
931                        }
932                    }
933                }
934            }
935        }
936        None
937    }
938
939    fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
940        schema.reference().or_else(|| schema.recursive_reference())
941    }
942
943    fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
944        if ref_str == "#" {
945            None // Special case for self-reference
946        } else {
947            ref_str.split('/').next_back()
948        }
949    }
950
951    fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
952        // Check cache first
953        if let Some(cached) = self.resolved_cache.get(schema_name) {
954            return Ok(cached.clone());
955        }
956
957        // Set current schema name for context
958        self.current_schema_name = Some(schema_name.to_string());
959
960        let schema = self
961            .schemas
962            .get(schema_name)
963            .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
964            .clone();
965
966        // Prevent infinite recursion with placeholder
967        self.resolved_cache.insert(
968            schema_name.to_string(),
969            AnalyzedSchema {
970                name: schema_name.to_string(),
971                original: serde_json::to_value(&schema).unwrap_or(Value::Null),
972                schema_type: SchemaType::Reference {
973                    target: "placeholder".to_string(),
974                },
975                dependencies: HashSet::new(),
976                nullable: false,
977                description: None,
978                default: None,
979            },
980        );
981
982        let analyzed = self.analyze_schema_value(&schema, schema_name)?;
983
984        // Update cache with real result
985        self.resolved_cache
986            .insert(schema_name.to_string(), analyzed.clone());
987
988        Ok(analyzed)
989    }
990
991    fn analyze_schema_value(
992        &mut self,
993        schema: &Schema,
994        schema_name: &str,
995    ) -> Result<AnalyzedSchema> {
996        let details = schema.details();
997        let description = details.description.clone();
998        let nullable = details.is_nullable();
999        let mut dependencies = HashSet::new();
1000
1001        let schema_type = match schema {
1002            Schema::Reference { reference, .. } => {
1003                let target = self
1004                    .extract_schema_name(reference)
1005                    .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
1006                    .to_string();
1007                dependencies.insert(target.clone());
1008                SchemaType::Reference { target }
1009            }
1010            Schema::RecursiveRef { recursive_ref, .. } => {
1011                // Handle recursive references
1012                if recursive_ref == "#" {
1013                    // Self-reference to the current schema
1014                    dependencies.insert(schema_name.to_string());
1015                    SchemaType::Reference {
1016                        target: schema_name.to_string(),
1017                    }
1018                } else {
1019                    // Handle other recursive reference patterns
1020                    let target = self
1021                        .extract_schema_name(recursive_ref)
1022                        .unwrap_or(schema_name)
1023                        .to_string();
1024                    dependencies.insert(target.clone());
1025                    SchemaType::Reference { target }
1026                }
1027            }
1028            Schema::Typed { schema_type, .. } => {
1029                match schema_type {
1030                    OpenApiSchemaType::String => {
1031                        if let Some(values) = details.string_enum_values() {
1032                            SchemaType::StringEnum { values }
1033                        } else {
1034                            SchemaType::Primitive {
1035                                rust_type: "String".to_string(),
1036                            }
1037                        }
1038                    }
1039                    OpenApiSchemaType::Integer => {
1040                        let rust_type =
1041                            self.get_number_rust_type(OpenApiSchemaType::Integer, details);
1042                        SchemaType::Primitive { rust_type }
1043                    }
1044                    OpenApiSchemaType::Number => {
1045                        let rust_type =
1046                            self.get_number_rust_type(OpenApiSchemaType::Number, details);
1047                        SchemaType::Primitive { rust_type }
1048                    }
1049                    OpenApiSchemaType::Boolean => SchemaType::Primitive {
1050                        rust_type: "bool".to_string(),
1051                    },
1052                    OpenApiSchemaType::Array => {
1053                        // Analyze array item type
1054                        self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1055                    }
1056                    OpenApiSchemaType::Object => {
1057                        // Check if this is a dynamic JSON object
1058                        if self.should_use_dynamic_json(schema) {
1059                            SchemaType::Primitive {
1060                                rust_type: "serde_json::Value".to_string(),
1061                            }
1062                        } else {
1063                            // Analyze object properties
1064                            self.analyze_object_schema(schema, &mut dependencies)?
1065                        }
1066                    }
1067                    _ => SchemaType::Primitive {
1068                        rust_type: "serde_json::Value".to_string(),
1069                    },
1070                }
1071            }
1072            Schema::AnyOf {
1073                any_of,
1074                discriminator,
1075                ..
1076            } => {
1077                // Handle anyOf patterns (nullable vs flexible union vs discriminated)
1078                self.analyze_anyof_union(
1079                    any_of,
1080                    discriminator.as_ref(),
1081                    &mut dependencies,
1082                    schema_name,
1083                )?
1084            }
1085            Schema::OneOf {
1086                one_of,
1087                discriminator,
1088                ..
1089            } => {
1090                // Handle oneOf discriminated unions
1091                self.analyze_oneof_union(one_of, discriminator.as_ref(), None, &mut dependencies)?
1092            }
1093            Schema::AllOf { all_of, .. } => {
1094                // Handle allOf composition (schema inheritance)
1095                self.analyze_allof_composition(all_of, &mut dependencies)?
1096            }
1097            Schema::Untyped { .. } => {
1098                // Try to infer type from structure
1099                if let Some(inferred) = schema.inferred_type() {
1100                    match inferred {
1101                        OpenApiSchemaType::Object => {
1102                            if self.should_use_dynamic_json(schema) {
1103                                SchemaType::Primitive {
1104                                    rust_type: "serde_json::Value".to_string(),
1105                                }
1106                            } else {
1107                                self.analyze_object_schema(schema, &mut dependencies)?
1108                            }
1109                        }
1110                        OpenApiSchemaType::String if details.is_string_enum() => {
1111                            SchemaType::StringEnum {
1112                                values: details.string_enum_values().unwrap_or_default(),
1113                            }
1114                        }
1115                        _ => SchemaType::Primitive {
1116                            rust_type: "serde_json::Value".to_string(),
1117                        },
1118                    }
1119                } else {
1120                    SchemaType::Primitive {
1121                        rust_type: "serde_json::Value".to_string(),
1122                    }
1123                }
1124            }
1125        };
1126
1127        Ok(AnalyzedSchema {
1128            name: schema_name.to_string(),
1129            original: serde_json::to_value(schema).unwrap_or(Value::Null), // Convert back to Value for now
1130            schema_type,
1131            dependencies,
1132            nullable,
1133            description,
1134            default: details.default.clone(),
1135        })
1136    }
1137
1138    fn analyze_object_schema(
1139        &mut self,
1140        schema: &Schema,
1141        dependencies: &mut HashSet<String>,
1142    ) -> Result<SchemaType> {
1143        let details = schema.details();
1144        let properties = &details.properties;
1145        let required = details
1146            .required
1147            .as_ref()
1148            .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1149            .unwrap_or_default();
1150
1151        let mut property_info = BTreeMap::new();
1152
1153        if let Some(props) = properties {
1154            for (prop_name, prop_schema) in props {
1155                // Check if this property is a union that needs a named type
1156                let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1157                    // First check if this should be a dynamic JSON pattern
1158                    if self.should_use_dynamic_json(prop_schema) {
1159                        // This is a dynamic JSON pattern, use serde_json::Value directly
1160                        SchemaType::Primitive {
1161                            rust_type: "serde_json::Value".to_string(),
1162                        }
1163                    } else {
1164                        // This is an anyOf union in a property - create a named union type
1165                        // Use the current schema name as context to make the union name unique
1166                        let context_name = self
1167                            .current_schema_name
1168                            .clone()
1169                            .unwrap_or_else(|| "Unknown".to_string());
1170
1171                        // Generate a name based on both the schema and property name
1172                        let prop_pascal = self.to_pascal_case(prop_name);
1173                        let union_type_name = format!("{context_name}{prop_pascal}");
1174
1175                        // Analyze the union
1176                        let union_schema_type = self.analyze_anyof_union(
1177                            any_of,
1178                            prop_schema.discriminator(),
1179                            dependencies,
1180                            &union_type_name,
1181                        )?;
1182
1183                        // Store the union as a named schema
1184                        self.resolved_cache.insert(
1185                            union_type_name.clone(),
1186                            AnalyzedSchema {
1187                                name: union_type_name.clone(),
1188                                original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1189                                schema_type: union_schema_type,
1190                                dependencies: HashSet::new(),
1191                                nullable: false,
1192                                description: prop_schema.details().description.clone(),
1193                                default: None,
1194                            },
1195                        );
1196
1197                        // Return a reference to the named union type
1198                        dependencies.insert(union_type_name.clone());
1199                        SchemaType::Reference {
1200                            target: union_type_name,
1201                        }
1202                    }
1203                } else if let Schema::OneOf {
1204                    one_of,
1205                    discriminator,
1206                    ..
1207                } = prop_schema
1208                {
1209                    // Handle oneOf discriminated unions in properties
1210                    // Generate a name based on the property name
1211                    let context_name = self
1212                        .current_schema_name
1213                        .clone()
1214                        .unwrap_or_else(|| "Unknown".to_string());
1215                    let prop_pascal = self.to_pascal_case(prop_name);
1216                    let union_type_name = format!("{context_name}{prop_pascal}");
1217
1218                    // Analyze the discriminated union
1219                    let union_schema_type = self.analyze_oneof_union(
1220                        one_of,
1221                        discriminator.as_ref(),
1222                        Some(&union_type_name),
1223                        dependencies,
1224                    )?;
1225
1226                    // Store the union as a named schema
1227                    self.resolved_cache.insert(
1228                        union_type_name.clone(),
1229                        AnalyzedSchema {
1230                            name: union_type_name.clone(),
1231                            original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1232                            schema_type: union_schema_type,
1233                            dependencies: HashSet::new(),
1234                            nullable: false,
1235                            description: prop_schema.details().description.clone(),
1236                            default: None,
1237                        },
1238                    );
1239
1240                    // Return a reference to the named union type
1241                    dependencies.insert(union_type_name.clone());
1242                    SchemaType::Reference {
1243                        target: union_type_name,
1244                    }
1245                } else {
1246                    // Regular property schema analysis - pass property name for context
1247                    self.analyze_property_schema_with_context(
1248                        prop_schema,
1249                        Some(prop_name),
1250                        dependencies,
1251                    )?
1252                };
1253
1254                let prop_details = prop_schema.details();
1255                // Check for both explicit nullable and anyOf nullable patterns
1256                let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1257                let prop_description = prop_details.description.clone();
1258                let prop_default = prop_details.default.clone();
1259
1260                property_info.insert(
1261                    prop_name.clone(),
1262                    PropertyInfo {
1263                        schema_type: prop_type,
1264                        nullable: prop_nullable,
1265                        description: prop_description,
1266                        default: prop_default,
1267                        serde_attrs: Vec::new(),
1268                    },
1269                );
1270            }
1271        }
1272
1273        // Check additionalProperties setting
1274        let additional_properties = match &details.additional_properties {
1275            Some(crate::openapi::AdditionalProperties::Boolean(true)) => true,
1276            Some(crate::openapi::AdditionalProperties::Boolean(false)) => false,
1277            Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1278                // For now, treat schema-based additionalProperties as true
1279                // TODO: Could analyze the schema to determine the value type
1280                true
1281            }
1282            None => false, // Default is false if not specified
1283        };
1284
1285        Ok(SchemaType::Object {
1286            properties: property_info,
1287            required,
1288            additional_properties,
1289        })
1290    }
1291
1292    fn analyze_property_schema_with_context(
1293        &mut self,
1294        schema: &Schema,
1295        property_name: Option<&str>,
1296        dependencies: &mut HashSet<String>,
1297    ) -> Result<SchemaType> {
1298        if let Some(ref_str) = self.get_any_reference(schema) {
1299            let target = if ref_str == "#" {
1300                // $recursiveRef: "#" - need to find the schema with $recursiveAnchor: true
1301                self.find_recursive_anchor_schema()
1302                    .unwrap_or_else(|| "UnknownRecursive".to_string())
1303            } else {
1304                self.extract_schema_name(ref_str)
1305                    .ok_or_else(|| GeneratorError::UnresolvedReference(ref_str.to_string()))?
1306                    .to_string()
1307            };
1308            dependencies.insert(target.clone());
1309            return Ok(SchemaType::Reference { target });
1310        }
1311
1312        if let Some(schema_type) = schema.schema_type() {
1313            match schema_type {
1314                OpenApiSchemaType::String => {
1315                    // Check if this string type has enum values
1316                    if let Some(enum_values) = schema.details().string_enum_values() {
1317                        // This is an inline enum in a property - create a named enum type
1318                        // Use the current schema name as context to make the enum name unique
1319                        let context_name = self
1320                            .current_schema_name
1321                            .clone()
1322                            .unwrap_or_else(|| "Unknown".to_string());
1323
1324                        // Generate a unique name based on both the schema and property context
1325                        let enum_type_name = if let Some(prop_name) = property_name {
1326                            // We have property name context - use it for a unique name
1327                            let prop_pascal = self.to_pascal_case(prop_name);
1328                            format!("{context_name}{prop_pascal}")
1329                        } else {
1330                            // No property name context - generate a unique name using enum values
1331                            // Use the first enum value to help make the name unique
1332                            let suffix = if !enum_values.is_empty() {
1333                                let first_value = self.to_pascal_case(&enum_values[0]);
1334                                format!("{first_value}Enum")
1335                            } else {
1336                                "StringEnum".to_string()
1337                            };
1338                            format!("{context_name}{suffix}")
1339                        };
1340
1341                        // Check if this exact enum type was already created (for deduplication)
1342                        // Only reuse if the enum values are exactly the same
1343                        let should_create_new = !self
1344                            .resolved_cache
1345                            .get(&enum_type_name)
1346                            .map(|existing| {
1347                                if let SchemaType::StringEnum {
1348                                    values: existing_values,
1349                                } = &existing.schema_type
1350                                {
1351                                    existing_values == &enum_values
1352                                } else {
1353                                    false
1354                                }
1355                            })
1356                            .unwrap_or(false);
1357
1358                        if should_create_new {
1359                            // Store the enum as a named schema
1360                            self.resolved_cache.insert(
1361                                enum_type_name.clone(),
1362                                AnalyzedSchema {
1363                                    name: enum_type_name.clone(),
1364                                    original: serde_json::to_value(schema).unwrap_or(Value::Null),
1365                                    schema_type: SchemaType::StringEnum {
1366                                        values: enum_values,
1367                                    },
1368                                    dependencies: HashSet::new(),
1369                                    nullable: false,
1370                                    description: schema.details().description.clone(),
1371                                    default: schema.details().default.clone(),
1372                                },
1373                            );
1374                        }
1375
1376                        // Return a reference to the named enum type
1377                        dependencies.insert(enum_type_name.clone());
1378                        return Ok(SchemaType::Reference {
1379                            target: enum_type_name,
1380                        });
1381                    } else {
1382                        return Ok(SchemaType::Primitive {
1383                            rust_type: "String".to_string(),
1384                        });
1385                    }
1386                }
1387                OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
1388                    let details = schema.details();
1389                    let rust_type = self.get_number_rust_type(schema_type.clone(), details);
1390                    return Ok(SchemaType::Primitive { rust_type });
1391                }
1392                OpenApiSchemaType::Boolean => {
1393                    return Ok(SchemaType::Primitive {
1394                        rust_type: "bool".to_string(),
1395                    });
1396                }
1397                OpenApiSchemaType::Array => {
1398                    // Analyze array property with context
1399                    let context_name = if let Some(prop_name) = property_name {
1400                        // Use property name for context
1401                        let prop_pascal = self.to_pascal_case(prop_name);
1402                        format!(
1403                            "{}{}",
1404                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1405                            prop_pascal
1406                        )
1407                    } else {
1408                        // Fallback to generic name
1409                        "ArrayItem".to_string()
1410                    };
1411                    return self.analyze_array_schema(schema, &context_name, dependencies);
1412                }
1413                OpenApiSchemaType::Object => {
1414                    // Check if this is a dynamic JSON object
1415                    if self.should_use_dynamic_json(schema) {
1416                        return Ok(SchemaType::Primitive {
1417                            rust_type: "serde_json::Value".to_string(),
1418                        });
1419                    }
1420                    // Inline object in property - create a named schema for it
1421                    let object_type_name = if let Some(prop_name) = property_name {
1422                        // Use property name for context
1423                        let prop_pascal = self.to_pascal_case(prop_name);
1424                        format!(
1425                            "{}{}",
1426                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1427                            prop_pascal
1428                        )
1429                    } else {
1430                        // Fallback to generic name
1431                        format!(
1432                            "{}Object",
1433                            self.current_schema_name.as_deref().unwrap_or("Unknown")
1434                        )
1435                    };
1436
1437                    // Analyze the object schema
1438                    let object_type = self.analyze_object_schema(schema, dependencies)?;
1439
1440                    // Create an analyzed schema for the inline object
1441                    let inline_schema = AnalyzedSchema {
1442                        name: object_type_name.clone(),
1443                        original: serde_json::to_value(schema).unwrap_or(Value::Null),
1444                        schema_type: object_type,
1445                        dependencies: dependencies.clone(),
1446                        nullable: false,
1447                        description: schema.details().description.clone(),
1448                        default: None,
1449                    };
1450
1451                    // Add the inline object as a named schema
1452                    self.resolved_cache
1453                        .insert(object_type_name.clone(), inline_schema);
1454                    dependencies.insert(object_type_name.clone());
1455
1456                    // Return a reference to the named schema
1457                    return Ok(SchemaType::Reference {
1458                        target: object_type_name,
1459                    });
1460                }
1461                _ => {
1462                    return Ok(SchemaType::Primitive {
1463                        rust_type: "serde_json::Value".to_string(),
1464                    });
1465                }
1466            }
1467        }
1468
1469        // Handle nullable patterns
1470        if schema.is_nullable_pattern() {
1471            if let Some(non_null) = schema.non_null_variant() {
1472                return self.analyze_property_schema_with_context(
1473                    non_null,
1474                    property_name,
1475                    dependencies,
1476                );
1477            }
1478        }
1479
1480        // Check if this should be dynamic JSON before further analysis
1481        if self.should_use_dynamic_json(schema) {
1482            return Ok(SchemaType::Primitive {
1483                rust_type: "serde_json::Value".to_string(),
1484            });
1485        }
1486
1487        // Handle allOf composition patterns
1488        if let Schema::AllOf { all_of, .. } = schema {
1489            return self.analyze_allof_composition(all_of, dependencies);
1490        }
1491
1492        // Handle union patterns (anyOf/oneOf) that weren't caught earlier
1493        if let Some(variants) = schema.union_variants() {
1494            match variants.len().cmp(&1) {
1495                std::cmp::Ordering::Equal => {
1496                    // Single variant - analyze it directly
1497                    return self.analyze_property_schema_with_context(
1498                        &variants[0],
1499                        property_name,
1500                        dependencies,
1501                    );
1502                }
1503                std::cmp::Ordering::Greater => {
1504                    // Multiple variants - try to analyze as a union
1505                    // Generate a context-aware name for the union type
1506                    let union_name = if let Some(prop_name) = property_name {
1507                        // We have property context - create a proper union name
1508                        let prop_pascal = self.to_pascal_case(prop_name);
1509                        format!(
1510                            "{}{}",
1511                            self.current_schema_name.as_deref().unwrap_or(""),
1512                            prop_pascal
1513                        )
1514                    } else {
1515                        "UnionType".to_string()
1516                    };
1517
1518                    // Check if this is a oneOf or anyOf
1519                    if let Schema::OneOf {
1520                        one_of,
1521                        discriminator,
1522                        ..
1523                    } = schema
1524                    {
1525                        // This is a oneOf - analyze it properly with potential discriminator
1526                        let oneof_result = self.analyze_oneof_union(
1527                            one_of,
1528                            discriminator.as_ref(),
1529                            Some(&union_name),
1530                            dependencies,
1531                        )?;
1532
1533                        // If we got a union type (not discriminated), we need to store it as a named type
1534                        if let SchemaType::Union {
1535                            variants: _union_variants,
1536                        } = &oneof_result
1537                        {
1538                            // Store the union as a named type in resolved_cache
1539                            self.resolved_cache.insert(
1540                                union_name.clone(),
1541                                AnalyzedSchema {
1542                                    name: union_name.clone(),
1543                                    original: serde_json::to_value(schema).unwrap_or(Value::Null),
1544                                    schema_type: oneof_result.clone(),
1545                                    dependencies: dependencies.clone(),
1546                                    nullable: false,
1547                                    description: schema.details().description.clone(),
1548                                    default: None,
1549                                },
1550                            );
1551
1552                            // Return a reference to the named union type
1553                            dependencies.insert(union_name.clone());
1554                            return Ok(SchemaType::Reference { target: union_name });
1555                        }
1556
1557                        return Ok(oneof_result);
1558                    } else if let Schema::AnyOf {
1559                        any_of,
1560                        discriminator,
1561                        ..
1562                    } = schema
1563                    {
1564                        // This is anyOf - use existing logic with discriminator support
1565                        let union_analysis = self.analyze_anyof_union(
1566                            any_of,
1567                            discriminator.as_ref(),
1568                            dependencies,
1569                            &union_name,
1570                        )?;
1571                        return Ok(union_analysis);
1572                    } else {
1573                        // This shouldn't happen, but handle gracefully
1574                        // Create a simple union from variants
1575                        let mut union_variants = Vec::new();
1576                        for variant in variants {
1577                            if let Some(ref_str) = variant.reference() {
1578                                if let Some(target) = self.extract_schema_name(ref_str) {
1579                                    dependencies.insert(target.to_string());
1580                                    union_variants.push(SchemaRef {
1581                                        target: target.to_string(),
1582                                        nullable: false,
1583                                    });
1584                                }
1585                            }
1586                        }
1587                        return Ok(SchemaType::Union {
1588                            variants: union_variants,
1589                        });
1590                    }
1591                }
1592                std::cmp::Ordering::Less => {}
1593            }
1594        }
1595
1596        // Handle untyped schemas by trying to infer from structure
1597        if let Some(inferred_type) = schema.inferred_type() {
1598            match inferred_type {
1599                OpenApiSchemaType::Object => {
1600                    // Double-check for dynamic JSON pattern even for inferred objects
1601                    if self.should_use_dynamic_json(schema) {
1602                        return Ok(SchemaType::Primitive {
1603                            rust_type: "serde_json::Value".to_string(),
1604                        });
1605                    }
1606                    return self.analyze_object_schema(schema, dependencies);
1607                }
1608                OpenApiSchemaType::Array => {
1609                    let context_name = if let Some(prop_name) = property_name {
1610                        // Use property name for context
1611                        let prop_pascal = self.to_pascal_case(prop_name);
1612                        format!(
1613                            "{}{}",
1614                            self.current_schema_name.as_deref().unwrap_or("Unknown"),
1615                            prop_pascal
1616                        )
1617                    } else {
1618                        // Fallback to generic name
1619                        "ArrayItem".to_string()
1620                    };
1621                    return self.analyze_array_schema(schema, &context_name, dependencies);
1622                }
1623                OpenApiSchemaType::String => {
1624                    if let Some(enum_values) = schema.details().string_enum_values() {
1625                        return Ok(SchemaType::StringEnum {
1626                            values: enum_values,
1627                        });
1628                    } else {
1629                        return Ok(SchemaType::Primitive {
1630                            rust_type: "String".to_string(),
1631                        });
1632                    }
1633                }
1634                _ => {
1635                    // Handle other inferred types
1636                    let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
1637                    return Ok(SchemaType::Primitive { rust_type });
1638                }
1639            }
1640        }
1641
1642        Ok(SchemaType::Primitive {
1643            rust_type: "serde_json::Value".to_string(),
1644        })
1645    }
1646
1647    fn analyze_allof_composition(
1648        &mut self,
1649        all_of_schemas: &[Schema],
1650        dependencies: &mut HashSet<String>,
1651    ) -> Result<SchemaType> {
1652        // Special case: if allOf contains only a single reference, treat it as a direct type alias
1653        // This handles patterns like: "allOf": [{"$ref": "#/components/schemas/Usage"}]
1654        if all_of_schemas.len() == 1 {
1655            if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
1656                if let Some(target) = self.extract_schema_name(reference) {
1657                    dependencies.insert(target.to_string());
1658                    return Ok(SchemaType::Reference {
1659                        target: target.to_string(),
1660                    });
1661                }
1662            }
1663        }
1664
1665        // AllOf represents schema composition - merge all schemas into one
1666        let mut merged_properties = BTreeMap::new();
1667        let mut merged_required = HashSet::new();
1668        let mut descriptions = Vec::new();
1669
1670        // Save the current schema context to restore it when analyzing properties
1671        let current_context = self.current_schema_name.clone();
1672
1673        for schema in all_of_schemas {
1674            match schema {
1675                Schema::Reference { reference, .. } => {
1676                    // Add dependency on referenced schema
1677                    if let Some(target) = self.extract_schema_name(reference) {
1678                        dependencies.insert(target.to_string());
1679
1680                        // First ensure the referenced schema is analyzed
1681                        let analyzed_ref = self.analyze_schema(target)?;
1682
1683                        // Now merge the analyzed schema's properties
1684                        match &analyzed_ref.schema_type {
1685                            SchemaType::Object {
1686                                properties,
1687                                required,
1688                                ..
1689                            } => {
1690                                // Merge properties from the analyzed schema
1691                                for (prop_name, prop_info) in properties {
1692                                    merged_properties.insert(prop_name.clone(), prop_info.clone());
1693                                }
1694                                // Merge required fields
1695                                for req in required {
1696                                    merged_required.insert(req.clone());
1697                                }
1698                            }
1699                            _ => {
1700                                // If the referenced schema is not an object, fall back to raw merge
1701                                if let Some(ref_schema) = self.schemas.get(target).cloned() {
1702                                    self.merge_schema_into_properties(
1703                                        &ref_schema,
1704                                        &mut merged_properties,
1705                                        &mut merged_required,
1706                                        dependencies,
1707                                    )?;
1708                                }
1709                            }
1710                        }
1711                    }
1712                }
1713                Schema::Typed {
1714                    schema_type: OpenApiSchemaType::Object,
1715                    ..
1716                }
1717                | Schema::Untyped { .. } => {
1718                    // Restore the original context when analyzing inline properties
1719                    let saved_context = self.current_schema_name.clone();
1720                    self.current_schema_name = current_context.clone();
1721
1722                    // Merge object properties directly
1723                    self.merge_schema_into_properties(
1724                        schema,
1725                        &mut merged_properties,
1726                        &mut merged_required,
1727                        dependencies,
1728                    )?;
1729
1730                    // Restore the previous context
1731                    self.current_schema_name = saved_context;
1732                }
1733                _ => {
1734                    // For non-object typed schemas in allOf, try to merge them as well
1735                    // This handles cases like allOf with enum or string constraints
1736                    self.merge_schema_into_properties(
1737                        schema,
1738                        &mut merged_properties,
1739                        &mut merged_required,
1740                        dependencies,
1741                    )?;
1742                }
1743            }
1744
1745            // Collect descriptions
1746            if let Some(desc) = &schema.details().description {
1747                descriptions.push(desc.clone());
1748            }
1749        }
1750
1751        // If we successfully merged properties, return an object
1752        if !merged_properties.is_empty() {
1753            Ok(SchemaType::Object {
1754                properties: merged_properties,
1755                required: merged_required,
1756                additional_properties: false,
1757            })
1758        } else {
1759            // Fall back to composition if we couldn't merge
1760            Ok(SchemaType::Composition {
1761                schemas: all_of_schemas
1762                    .iter()
1763                    .filter_map(|s| {
1764                        if let Some(ref_str) = s.reference() {
1765                            if let Some(target) = self.extract_schema_name(ref_str) {
1766                                dependencies.insert(target.to_string());
1767                                Some(SchemaRef {
1768                                    target: target.to_string(),
1769                                    nullable: false,
1770                                })
1771                            } else {
1772                                None
1773                            }
1774                        } else {
1775                            None
1776                        }
1777                    })
1778                    .collect(),
1779            })
1780        }
1781    }
1782
1783    fn merge_schema_into_properties(
1784        &mut self,
1785        schema: &Schema,
1786        merged_properties: &mut BTreeMap<String, PropertyInfo>,
1787        merged_required: &mut HashSet<String>,
1788        dependencies: &mut HashSet<String>,
1789    ) -> Result<()> {
1790        let details = schema.details();
1791
1792        // Merge properties
1793        if let Some(properties) = &details.properties {
1794            for (prop_name, prop_schema) in properties {
1795                let prop_type = self.analyze_property_schema_with_context(
1796                    prop_schema,
1797                    Some(prop_name),
1798                    dependencies,
1799                )?;
1800                let prop_details = prop_schema.details();
1801
1802                merged_properties.insert(
1803                    prop_name.clone(),
1804                    PropertyInfo {
1805                        schema_type: prop_type,
1806                        nullable: prop_details.is_nullable(),
1807                        description: prop_details.description.clone(),
1808                        default: prop_details.default.clone(),
1809                        serde_attrs: Vec::new(),
1810                    },
1811                );
1812            }
1813        }
1814
1815        // Merge required fields
1816        if let Some(required) = &details.required {
1817            for field in required {
1818                merged_required.insert(field.clone());
1819            }
1820        }
1821
1822        Ok(())
1823    }
1824
1825    fn analyze_oneof_union(
1826        &mut self,
1827        one_of_schemas: &[Schema],
1828        discriminator: Option<&crate::openapi::Discriminator>,
1829        parent_name: Option<&str>,
1830        dependencies: &mut HashSet<String>,
1831    ) -> Result<SchemaType> {
1832        // If there's no discriminator, we should create an untagged union
1833        if discriminator.is_none() {
1834            // Handle untagged unions (oneOf without discriminator)
1835            return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
1836        }
1837
1838        // This is a discriminated union
1839        let discriminator_field = discriminator
1840            .ok_or_else(|| {
1841                GeneratorError::InvalidDiscriminator(
1842                    "expected discriminator after guard check".to_string(),
1843                )
1844            })?
1845            .property_name
1846            .clone();
1847
1848        let mut variants = Vec::new();
1849        let mut used_variant_names = std::collections::HashSet::new();
1850
1851        for variant_schema in one_of_schemas {
1852            // Check if this is a direct reference, recursive reference, or an allOf wrapper with a reference
1853            let ref_info = if let Some(ref_str) = variant_schema.reference() {
1854                Some((ref_str, false))
1855            } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
1856                Some((recursive_ref, true))
1857            } else if let Schema::AllOf { all_of, .. } = variant_schema {
1858                // Check if this is an allOf with a single reference
1859                if all_of.len() == 1 {
1860                    if let Some(ref_str) = all_of[0].reference() {
1861                        Some((ref_str, false))
1862                    } else {
1863                        all_of[0]
1864                            .recursive_reference()
1865                            .map(|recursive_ref| (recursive_ref, true))
1866                    }
1867                } else {
1868                    None
1869                }
1870            } else {
1871                None
1872            };
1873
1874            if let Some((ref_str, is_recursive)) = ref_info {
1875                let schema_name = if is_recursive && ref_str == "#" {
1876                    // Handle recursive reference to the schema with recursiveAnchor
1877                    self.find_recursive_anchor_schema()
1878                        .or_else(|| self.current_schema_name.clone())
1879                        .unwrap_or_else(|| "CompoundFilter".to_string())
1880                } else {
1881                    self.extract_schema_name(ref_str)
1882                        .map(|s| s.to_string())
1883                        .unwrap_or_else(|| "UnknownRef".to_string())
1884                };
1885
1886                if !schema_name.is_empty() {
1887                    dependencies.insert(schema_name.clone());
1888
1889                    // Determine discriminator value with priority order:
1890                    // 1. Explicit mapping in discriminator
1891                    // 2. Extract from referenced schema
1892                    // 3. Generate from schema name
1893                    let discriminator_value = if let Some(disc) = discriminator {
1894                        if let Some(mappings) = &disc.mapping {
1895                            // Find the mapping key that points to this schema reference
1896                            // Mapping format is: "discriminator_value" -> "#/components/schemas/SchemaName"
1897                            mappings
1898                                .iter()
1899                                .find(|(_, target_ref)| {
1900                                    // Check if this mapping target matches our reference
1901                                    target_ref.as_str() == ref_str
1902                                        || self
1903                                            .extract_schema_name(target_ref)
1904                                            .map(|s| s.to_string())
1905                                            == Some(schema_name.clone())
1906                                })
1907                                .map(|(key, _)| key.clone())
1908                                .unwrap_or_else(|| {
1909                                    self.fallback_discriminator_value_for_field(
1910                                        &schema_name,
1911                                        &discriminator_field,
1912                                    )
1913                                })
1914                        } else {
1915                            self.fallback_discriminator_value_for_field(
1916                                &schema_name,
1917                                &discriminator_field,
1918                            )
1919                        }
1920                    } else {
1921                        self.fallback_discriminator_value_for_field(
1922                            &schema_name,
1923                            &discriminator_field,
1924                        )
1925                    };
1926
1927                    // Generate Rust-friendly variant name and ensure uniqueness
1928                    let base_name = self.to_rust_variant_name(&schema_name);
1929                    let rust_name =
1930                        self.ensure_unique_variant_name(base_name, &mut used_variant_names);
1931
1932                    // Use the discriminator value as-is from the schema
1933                    let final_discriminator_value = discriminator_value;
1934
1935                    variants.push(UnionVariant {
1936                        rust_name,
1937                        type_name: schema_name,
1938                        discriminator_value: final_discriminator_value,
1939                        schema_ref: ref_str.to_string(),
1940                    });
1941                }
1942            } else {
1943                // Handle inline schemas in oneOf
1944                let variant_index = variants.len();
1945                let inline_type_name =
1946                    self.generate_inline_type_name(variant_schema, variant_index);
1947
1948                // Try to extract discriminator value from inline schema
1949                let discriminator_value = if let Some(disc) = discriminator {
1950                    if let Some(mappings) = &disc.mapping {
1951                        // Look for mapping that points to this inline variant by index
1952                        mappings
1953                            .iter()
1954                            .find(|(_, target_ref)| {
1955                                target_ref.contains(&format!("variant_{variant_index}"))
1956                            })
1957                            .map(|(key, _)| key.clone())
1958                            .unwrap_or_else(|| {
1959                                self.extract_inline_discriminator_value(
1960                                    variant_schema,
1961                                    &discriminator_field,
1962                                    variant_index,
1963                                )
1964                            })
1965                    } else {
1966                        self.extract_inline_discriminator_value(
1967                            variant_schema,
1968                            &discriminator_field,
1969                            variant_index,
1970                        )
1971                    }
1972                } else {
1973                    self.extract_inline_discriminator_value(
1974                        variant_schema,
1975                        &discriminator_field,
1976                        variant_index,
1977                    )
1978                };
1979
1980                // Generate Rust-friendly variant name based on discriminator or fallback to generic
1981                let base_name = if discriminator_value.starts_with("variant_") {
1982                    format!("Variant{variant_index}")
1983                } else {
1984                    // Convert discriminator value to a meaningful Rust variant name
1985                    let clean_name = self.discriminator_to_variant_name(&discriminator_value);
1986                    self.to_rust_variant_name(&clean_name)
1987                };
1988                let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
1989
1990                // Use the discriminator value as-is from the schema
1991                let final_discriminator_value = discriminator_value;
1992
1993                variants.push(UnionVariant {
1994                    rust_name,
1995                    type_name: inline_type_name.clone(),
1996                    discriminator_value: final_discriminator_value,
1997                    schema_ref: format!("inline_{variant_index}"),
1998                });
1999
2000                // Store inline schema for later analysis and generation
2001                self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2002            }
2003        }
2004
2005        if variants.is_empty() {
2006            // If we couldn't create a discriminated union, fall back to an untagged union
2007            // This handles cases where oneOf contains references or inline schemas without proper discriminators
2008            let mut union_variants = Vec::new();
2009
2010            for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2011                // First check if it's a reference or recursive reference
2012                if let Some(ref_str) = variant_schema.reference() {
2013                    if let Some(schema_name) = self.extract_schema_name(ref_str) {
2014                        dependencies.insert(schema_name.to_string());
2015                        union_variants.push(SchemaRef {
2016                            target: schema_name.to_string(),
2017                            nullable: false,
2018                        });
2019                    }
2020                } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2021                    let schema_name = if recursive_ref == "#" {
2022                        // Handle recursive reference to the schema with recursiveAnchor
2023                        self.find_recursive_anchor_schema()
2024                            .or_else(|| self.current_schema_name.clone())
2025                            .unwrap_or_else(|| "CompoundFilter".to_string())
2026                    } else {
2027                        self.extract_schema_name(recursive_ref)
2028                            .map(|s| s.to_string())
2029                            .unwrap_or_else(|| "RecursiveType".to_string())
2030                    };
2031                    dependencies.insert(schema_name.clone());
2032                    union_variants.push(SchemaRef {
2033                        target: schema_name,
2034                        nullable: false,
2035                    });
2036                } else {
2037                    // Handle inline schemas by creating type aliases or using primitive types directly
2038                    let context = parent_name.unwrap_or("Union");
2039                    let inline_name = self.generate_context_aware_name(
2040                        context,
2041                        "InlineVariant",
2042                        variant_index,
2043                        Some(variant_schema),
2044                    );
2045                    let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2046                    let variant_type = analyzed.schema_type;
2047
2048                    // Add dependencies from the analyzed schema
2049                    for dep in &analyzed.dependencies {
2050                        dependencies.insert(dep.clone());
2051                    }
2052
2053                    match &variant_type {
2054                        // For primitive types, we can use them directly in the union
2055                        SchemaType::Primitive { rust_type } => {
2056                            union_variants.push(SchemaRef {
2057                                target: rust_type.clone(),
2058                                nullable: false,
2059                            });
2060                        }
2061                        // For arrays, check if we can determine the item type
2062                        SchemaType::Array { item_type } => {
2063                            match item_type.as_ref() {
2064                                SchemaType::Primitive { rust_type } => {
2065                                    let type_name = format!("Vec<{rust_type}>");
2066                                    union_variants.push(SchemaRef {
2067                                        target: type_name,
2068                                        nullable: false,
2069                                    });
2070                                }
2071                                SchemaType::Reference { target } => {
2072                                    let type_name = format!("Vec<{target}>");
2073                                    union_variants.push(SchemaRef {
2074                                        target: type_name,
2075                                        nullable: false,
2076                                    });
2077                                }
2078                                _ => {
2079                                    // For other array types, create an inline type
2080                                    let context = parent_name.unwrap_or("Inline");
2081                                    let inline_type_name = self.generate_context_aware_name(
2082                                        context,
2083                                        "Variant",
2084                                        variant_index,
2085                                        None,
2086                                    );
2087                                    self.add_inline_schema(
2088                                        &inline_type_name,
2089                                        variant_schema,
2090                                        dependencies,
2091                                    )?;
2092                                    union_variants.push(SchemaRef {
2093                                        target: inline_type_name,
2094                                        nullable: false,
2095                                    });
2096                                }
2097                            }
2098                        }
2099                        // For reference types, use the reference target directly
2100                        SchemaType::Reference { target } => {
2101                            union_variants.push(SchemaRef {
2102                                target: target.clone(),
2103                                nullable: false,
2104                            });
2105                        }
2106                        // For other complex types, create an inline type
2107                        _ => {
2108                            let inline_type_name = format!(
2109                                "{}Variant{}",
2110                                parent_name.unwrap_or("Inline"),
2111                                variant_index + 1
2112                            );
2113                            self.add_inline_schema(
2114                                &inline_type_name,
2115                                variant_schema,
2116                                dependencies,
2117                            )?;
2118                            union_variants.push(SchemaRef {
2119                                target: inline_type_name,
2120                                nullable: false,
2121                            });
2122                        }
2123                    }
2124                }
2125            }
2126
2127            if !union_variants.is_empty() {
2128                return Ok(SchemaType::Union {
2129                    variants: union_variants,
2130                });
2131            }
2132
2133            // Only fall back to serde_json::Value if we truly can't analyze the union
2134            return Ok(SchemaType::Primitive {
2135                rust_type: "serde_json::Value".to_string(),
2136            });
2137        }
2138
2139        Ok(SchemaType::DiscriminatedUnion {
2140            discriminator_field,
2141            variants,
2142        })
2143    }
2144
2145    fn analyze_untagged_oneof_union(
2146        &mut self,
2147        one_of_schemas: &[Schema],
2148        parent_name: Option<&str>,
2149        dependencies: &mut HashSet<String>,
2150    ) -> Result<SchemaType> {
2151        let mut union_variants = Vec::new();
2152
2153        for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2154            // First check if it's a reference or recursive reference
2155            if let Some(ref_str) = variant_schema.reference() {
2156                if let Some(schema_name) = self.extract_schema_name(ref_str) {
2157                    dependencies.insert(schema_name.to_string());
2158                    union_variants.push(SchemaRef {
2159                        target: schema_name.to_string(),
2160                        nullable: false,
2161                    });
2162                }
2163            } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2164                let schema_name = if recursive_ref == "#" {
2165                    // Handle recursive reference to the schema with recursiveAnchor
2166                    self.find_recursive_anchor_schema()
2167                        .or_else(|| self.current_schema_name.clone())
2168                        .unwrap_or_else(|| "CompoundFilter".to_string())
2169                } else {
2170                    self.extract_schema_name(recursive_ref)
2171                        .map(|s| s.to_string())
2172                        .unwrap_or_else(|| "RecursiveType".to_string())
2173                };
2174                dependencies.insert(schema_name.clone());
2175                union_variants.push(SchemaRef {
2176                    target: schema_name,
2177                    nullable: false,
2178                });
2179            } else {
2180                // Handle inline schemas by creating type aliases or using primitive types directly
2181                let context = parent_name.unwrap_or("Union");
2182                let inline_name = self.generate_context_aware_name(
2183                    context,
2184                    "InlineVariant",
2185                    variant_index,
2186                    Some(variant_schema),
2187                );
2188                let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2189                let variant_type = analyzed.schema_type;
2190
2191                // Add dependencies from the analyzed schema
2192                for dep in &analyzed.dependencies {
2193                    dependencies.insert(dep.clone());
2194                }
2195
2196                match &variant_type {
2197                    // For primitive types, we can use them directly in the union
2198                    SchemaType::Primitive { rust_type } => {
2199                        union_variants.push(SchemaRef {
2200                            target: rust_type.clone(),
2201                            nullable: false,
2202                        });
2203                    }
2204                    // For arrays, check if we can determine the item type
2205                    SchemaType::Array { item_type } => {
2206                        match item_type.as_ref() {
2207                            SchemaType::Primitive { rust_type } => {
2208                                let type_name = format!("Vec<{rust_type}>");
2209                                union_variants.push(SchemaRef {
2210                                    target: type_name,
2211                                    nullable: false,
2212                                });
2213                            }
2214                            SchemaType::Reference { target } => {
2215                                let type_name = format!("Vec<{target}>");
2216                                union_variants.push(SchemaRef {
2217                                    target: type_name,
2218                                    nullable: false,
2219                                });
2220                            }
2221                            // Handle arrays of arrays (e.g., Vec<Vec<i64>>)
2222                            SchemaType::Array {
2223                                item_type: inner_item_type,
2224                            } => {
2225                                match inner_item_type.as_ref() {
2226                                    SchemaType::Primitive { rust_type } => {
2227                                        let type_name = format!("Vec<Vec<{rust_type}>>");
2228                                        union_variants.push(SchemaRef {
2229                                            target: type_name,
2230                                            nullable: false,
2231                                        });
2232                                    }
2233                                    SchemaType::Reference { target } => {
2234                                        let type_name = format!("Vec<Vec<{target}>>");
2235                                        union_variants.push(SchemaRef {
2236                                            target: type_name,
2237                                            nullable: false,
2238                                        });
2239                                    }
2240                                    _ => {
2241                                        // For deeper nesting, create an inline type
2242                                        let context = parent_name.unwrap_or("Inline");
2243                                        let inline_type_name = self.generate_context_aware_name(
2244                                            context,
2245                                            "Variant",
2246                                            variant_index,
2247                                            None,
2248                                        );
2249                                        self.add_inline_schema(
2250                                            &inline_type_name,
2251                                            variant_schema,
2252                                            dependencies,
2253                                        )?;
2254                                        union_variants.push(SchemaRef {
2255                                            target: inline_type_name,
2256                                            nullable: false,
2257                                        });
2258                                    }
2259                                }
2260                            }
2261                            _ => {
2262                                // For other array types, create an inline type
2263                                let context = parent_name.unwrap_or("Inline");
2264                                let inline_type_name = self.generate_context_aware_name(
2265                                    context,
2266                                    "Variant",
2267                                    variant_index,
2268                                    None,
2269                                );
2270                                self.add_inline_schema(
2271                                    &inline_type_name,
2272                                    variant_schema,
2273                                    dependencies,
2274                                )?;
2275                                union_variants.push(SchemaRef {
2276                                    target: inline_type_name,
2277                                    nullable: false,
2278                                });
2279                            }
2280                        }
2281                    }
2282                    // For reference types, use the reference target directly
2283                    SchemaType::Reference { target } => {
2284                        union_variants.push(SchemaRef {
2285                            target: target.clone(),
2286                            nullable: false,
2287                        });
2288                    }
2289                    // For other complex types, create an inline type
2290                    _ => {
2291                        let context = parent_name.unwrap_or("Inline");
2292                        let inline_type_name = self.generate_context_aware_name(
2293                            context,
2294                            "Variant",
2295                            variant_index,
2296                            None,
2297                        );
2298                        self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2299                        union_variants.push(SchemaRef {
2300                            target: inline_type_name,
2301                            nullable: false,
2302                        });
2303                    }
2304                }
2305            }
2306        }
2307
2308        if !union_variants.is_empty() {
2309            return Ok(SchemaType::Union {
2310                variants: union_variants,
2311            });
2312        }
2313
2314        // Only fall back to serde_json::Value if we truly can't analyze the union
2315        Ok(SchemaType::Primitive {
2316            rust_type: "serde_json::Value".to_string(),
2317        })
2318    }
2319
2320    fn add_inline_schema(
2321        &mut self,
2322        type_name: &str,
2323        schema: &Schema,
2324        dependencies: &mut HashSet<String>,
2325    ) -> Result<()> {
2326        // For primitive types, we need to ensure they are stored as type aliases
2327        if let Some(schema_type) = schema.schema_type() {
2328            match schema_type {
2329                OpenApiSchemaType::String
2330                | OpenApiSchemaType::Integer
2331                | OpenApiSchemaType::Number
2332                | OpenApiSchemaType::Boolean => {
2333                    let rust_type =
2334                        self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
2335
2336                    // Store as a type alias
2337                    self.resolved_cache.insert(
2338                        type_name.to_string(),
2339                        AnalyzedSchema {
2340                            name: type_name.to_string(),
2341                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
2342                            schema_type: SchemaType::Primitive { rust_type },
2343                            dependencies: HashSet::new(),
2344                            nullable: false,
2345                            description: schema.details().description.clone(),
2346                            default: None,
2347                        },
2348                    );
2349                    return Ok(());
2350                }
2351                _ => {}
2352            }
2353        }
2354
2355        // For non-primitive types, analyze the inline schema and add it to our collection
2356        // Set current_schema_name so nested inline properties (enums, unions, objects)
2357        // get named with the correct parent context instead of inheriting a stale name
2358        let previous_schema_name = self.current_schema_name.take();
2359        self.current_schema_name = Some(type_name.to_string());
2360        let analyzed = self.analyze_schema_value(schema, type_name)?;
2361        self.current_schema_name = previous_schema_name;
2362
2363        // Add to resolved cache so it can be generated
2364        self.resolved_cache.insert(type_name.to_string(), analyzed);
2365
2366        // Add dependencies
2367        if let Some(cached) = self.resolved_cache.get(type_name) {
2368            for dep in &cached.dependencies {
2369                dependencies.insert(dep.clone());
2370            }
2371        }
2372
2373        Ok(())
2374    }
2375
2376    fn extract_inline_discriminator_value(
2377        &self,
2378        schema: &Schema,
2379        discriminator_field: &str,
2380        variant_index: usize,
2381    ) -> String {
2382        // Try to extract discriminator value from inline schema properties
2383        if let Some(properties) = &schema.details().properties {
2384            if let Some(discriminator_prop) = properties.get(discriminator_field) {
2385                // Check for enum with single value
2386                if let Some(enum_values) = &discriminator_prop.details().enum_values {
2387                    if enum_values.len() == 1 {
2388                        if let Some(value) = enum_values[0].as_str() {
2389                            return value.to_string();
2390                        }
2391                    }
2392                }
2393                // Check for const value in extra fields
2394                if let Some(const_value) = discriminator_prop.details().extra.get("const") {
2395                    if let Some(value) = const_value.as_str() {
2396                        return value.to_string();
2397                    }
2398                }
2399                // Check for const value in the discriminator_prop.details().const_value
2400                if let Some(const_value) = &discriminator_prop.details().const_value {
2401                    if let Some(value) = const_value.as_str() {
2402                        return value.to_string();
2403                    }
2404                }
2405            }
2406        }
2407
2408        // Try to infer from schema structure and properties
2409        if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
2410            return inferred_name;
2411        }
2412
2413        // Fall back to generic variant name
2414        format!("variant_{variant_index}")
2415    }
2416
2417    fn infer_variant_name_from_structure(
2418        &self,
2419        schema: &Schema,
2420        _variant_index: usize,
2421    ) -> Option<String> {
2422        let details = schema.details();
2423
2424        // Strategy 1: Look for unique property combinations that suggest the variant type
2425        if let Some(properties) = &details.properties {
2426            // Common patterns for content blocks
2427            if properties.contains_key("text") && properties.len() <= 3 {
2428                return Some("text".to_string());
2429            }
2430            if properties.contains_key("image") || properties.contains_key("source") {
2431                return Some("image".to_string());
2432            }
2433            if properties.contains_key("document") {
2434                return Some("document".to_string());
2435            }
2436            if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
2437                return Some("tool_result".to_string());
2438            }
2439            if properties.contains_key("content") && properties.contains_key("is_error") {
2440                return Some("tool_result".to_string());
2441            }
2442            if properties.contains_key("partial_json") {
2443                return Some("partial_json".to_string());
2444            }
2445
2446            // Strategy 2: Look for properties that hint at the variant purpose
2447            let property_names: Vec<&String> = properties.keys().collect();
2448
2449            // Try to find the most descriptive property name
2450            for prop_name in &property_names {
2451                if prop_name.contains("result") {
2452                    return Some("result".to_string());
2453                }
2454                if prop_name.contains("error") {
2455                    return Some("error".to_string());
2456                }
2457                if prop_name.contains("content") && property_names.len() <= 2 {
2458                    return Some("content".to_string());
2459                }
2460            }
2461
2462            // Strategy 3: Use the most significant unique property
2463            let significant_props = property_names
2464                .iter()
2465                .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
2466                .collect::<Vec<_>>();
2467
2468            if significant_props.len() == 1 {
2469                return Some((*significant_props[0]).clone());
2470            }
2471        }
2472
2473        // Strategy 4: Look at description for hints
2474        if let Some(description) = &details.description {
2475            let desc_lower = description.to_lowercase();
2476            if desc_lower.contains("text") && desc_lower.len() < 100 {
2477                return Some("text".to_string());
2478            }
2479            if desc_lower.contains("image") {
2480                return Some("image".to_string());
2481            }
2482            if desc_lower.contains("document") {
2483                return Some("document".to_string());
2484            }
2485            if desc_lower.contains("tool") && desc_lower.contains("result") {
2486                return Some("tool_result".to_string());
2487            }
2488        }
2489
2490        None
2491    }
2492
2493    fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
2494        // Convert discriminator values to PascalCase variant names using general rules
2495        if discriminator.is_empty() {
2496            return "Variant".to_string();
2497        }
2498
2499        let mut result = String::new();
2500        let mut next_upper = true;
2501
2502        for c in discriminator.chars() {
2503            match c {
2504                'a'..='z' => {
2505                    if next_upper {
2506                        result.push(c.to_ascii_uppercase());
2507                        next_upper = false;
2508                    } else {
2509                        result.push(c);
2510                    }
2511                }
2512                'A'..='Z' => {
2513                    result.push(c);
2514                    next_upper = false;
2515                }
2516                '0'..='9' => {
2517                    result.push(c);
2518                    next_upper = false;
2519                }
2520                '_' | '-' | '.' | ' ' | '/' | '\\' => {
2521                    // Word separators - next char should be uppercase
2522                    next_upper = true;
2523                }
2524                _ => {
2525                    // Other special characters - treat as word boundary
2526                    next_upper = true;
2527                }
2528            }
2529        }
2530
2531        // Ensure it starts with a letter
2532        if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2533            result = format!("Variant{result}");
2534        }
2535
2536        result
2537    }
2538
2539    fn ensure_unique_variant_name(
2540        &self,
2541        base_name: String,
2542        used_names: &mut std::collections::HashSet<String>,
2543    ) -> String {
2544        let mut candidate = base_name.clone();
2545        let mut counter = 1;
2546
2547        while used_names.contains(&candidate) {
2548            counter += 1;
2549            candidate = format!("{base_name}{counter}");
2550        }
2551
2552        used_names.insert(candidate.clone());
2553        candidate
2554    }
2555
2556    fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
2557        // Try to generate a meaningful name for inline schemas
2558        if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
2559            return meaningful_name;
2560        }
2561
2562        // Fallback to context-aware name
2563        let context = self.current_schema_name.as_deref().unwrap_or("Inline");
2564        self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
2565    }
2566
2567    fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
2568        let details = schema.details();
2569
2570        // Strategy 1: Use description if it's short and descriptive
2571        if let Some(description) = &details.description {
2572            if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
2573                return Some(name_from_desc);
2574            }
2575        }
2576
2577        // Strategy 2: Use the most significant property name as the type identifier
2578        if let Some(properties) = &details.properties {
2579            if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
2580                return Some(format!("{name_from_props}Block"));
2581            }
2582        }
2583
2584        None
2585    }
2586
2587    fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
2588        // Only use descriptions that are short and likely to be type identifiers
2589        if description.len() > 100 || description.contains('\n') {
2590            return None;
2591        }
2592
2593        // Extract the first meaningful word(s) from the description
2594        let words: Vec<&str> = description
2595            .split_whitespace()
2596            .take(2) // Only take first 2 words to avoid long names
2597            .filter(|word| {
2598                let w = word.to_lowercase();
2599                word.len() > 2
2600                    && ![
2601                        "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
2602                    ]
2603                    .contains(&w.as_str())
2604            })
2605            .collect();
2606
2607        if words.is_empty() {
2608            return None;
2609        }
2610
2611        // Convert to PascalCase using our existing logic
2612        let combined = words.join("_");
2613        let pascal_name = self.discriminator_to_variant_name(&combined);
2614
2615        // Add suffix if it doesn't already have one
2616        if !pascal_name.ends_with("Content")
2617            && !pascal_name.ends_with("Block")
2618            && !pascal_name.ends_with("Type")
2619        {
2620            Some(format!("{pascal_name}Content"))
2621        } else {
2622            Some(pascal_name)
2623        }
2624    }
2625
2626    fn extract_type_name_from_properties(
2627        &self,
2628        properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
2629    ) -> Option<String> {
2630        // Get property names, excluding common structural properties
2631        let significant_props: Vec<&String> = properties
2632            .keys()
2633            .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
2634            .collect();
2635
2636        if significant_props.is_empty() {
2637            return None;
2638        }
2639
2640        // Strategy 1: If there's only one significant property, use it
2641        if significant_props.len() == 1 {
2642            let prop_name = significant_props[0];
2643            return Some(self.discriminator_to_variant_name(prop_name));
2644        }
2645
2646        // Strategy 2: Use the first property alphabetically for consistency
2647        // This provides deterministic naming without hardcoded preferences
2648        let mut sorted_props = significant_props.clone();
2649        sorted_props.sort();
2650        if let Some(first_prop) = sorted_props.first() {
2651            return Some(self.discriminator_to_variant_name(first_prop));
2652        }
2653
2654        None
2655    }
2656
2657    fn openapi_type_to_rust_type(
2658        &self,
2659        openapi_type: OpenApiSchemaType,
2660        details: &crate::openapi::SchemaDetails,
2661    ) -> String {
2662        match openapi_type {
2663            OpenApiSchemaType::String => "String".to_string(),
2664            OpenApiSchemaType::Integer => self.get_number_rust_type(openapi_type, details),
2665            OpenApiSchemaType::Number => self.get_number_rust_type(openapi_type, details),
2666            OpenApiSchemaType::Boolean => "bool".to_string(),
2667            OpenApiSchemaType::Array => "Vec<serde_json::Value>".to_string(), // Fallback for arrays without items
2668            OpenApiSchemaType::Object => "serde_json::Value".to_string(), // Fallback for untyped objects
2669            OpenApiSchemaType::Null => "()".to_string(),                  // Null type
2670        }
2671    }
2672
2673    #[allow(dead_code)]
2674    fn fallback_discriminator_value(&self, schema_name: &str) -> String {
2675        self.fallback_discriminator_value_for_field(schema_name, "type")
2676    }
2677
2678    fn fallback_discriminator_value_for_field(
2679        &self,
2680        schema_name: &str,
2681        field_name: &str,
2682    ) -> String {
2683        // Try to extract from referenced schema first
2684        if let Some(ref_schema) = self.schemas.get(schema_name) {
2685            if let Some(extracted) =
2686                self.extract_discriminator_value_for_field(ref_schema, field_name)
2687            {
2688                return extracted;
2689            }
2690        }
2691
2692        // Fall back to generating from name
2693        self.generate_discriminator_value_from_name(schema_name)
2694    }
2695
2696    fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
2697        // Convert schema names like "ResponseCreatedEvent" to "response.created"
2698        let mut result = String::new();
2699        let mut chars = schema_name.chars().peekable();
2700        let mut first = true;
2701
2702        while let Some(c) = chars.next() {
2703            if c.is_uppercase()
2704                && !first
2705                && chars
2706                    .peek()
2707                    .map(|&next| next.is_lowercase())
2708                    .unwrap_or(false)
2709            {
2710                result.push('.');
2711            }
2712            result.push(c.to_ascii_lowercase());
2713            first = false;
2714        }
2715
2716        // Remove common suffixes
2717        if result.ends_with("event") {
2718            result = result[..result.len() - 5].to_string();
2719        }
2720
2721        // Add "response." prefix if it looks like a response event
2722        if schema_name.starts_with("Response") && !result.starts_with("response.") {
2723            result = format!("response.{}", result.trim_start_matches("response"));
2724        }
2725
2726        result
2727    }
2728
2729    fn to_rust_variant_name(&self, schema_name: &str) -> String {
2730        // Convert "ResponseCreatedEvent" to "Created", "UserStatus" to "UserStatus", etc.
2731        let mut name = schema_name;
2732
2733        // Remove common prefixes for cleaner variant names
2734        if name.starts_with("Response") && name.len() > 8 {
2735            name = &name[8..]; // Remove "Response"
2736        }
2737
2738        // Remove common suffixes
2739        if name.ends_with("Event") && name.len() > 5 {
2740            name = &name[..name.len() - 5]; // Remove "Event"
2741        }
2742
2743        // Trim leading and trailing underscores
2744        name = name.trim_matches('_');
2745
2746        // Convert underscores to camel case using our existing function
2747        if name.is_empty() {
2748            schema_name.to_string()
2749        } else {
2750            // Use discriminator_to_variant_name to properly handle underscores
2751            self.discriminator_to_variant_name(name)
2752        }
2753    }
2754
2755    fn analyze_array_schema(
2756        &mut self,
2757        schema: &Schema,
2758        parent_schema_name: &str,
2759        dependencies: &mut HashSet<String>,
2760    ) -> Result<SchemaType> {
2761        let details = schema.details();
2762
2763        // Check if items field is present
2764        if let Some(items_schema) = &details.items {
2765            // Analyze the item type
2766            let item_type = match items_schema.as_ref() {
2767                Schema::Reference { reference, .. } => {
2768                    // Array of referenced types
2769                    let target = self
2770                        .extract_schema_name(reference)
2771                        .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
2772                        .to_string();
2773                    dependencies.insert(target.clone());
2774                    SchemaType::Reference { target }
2775                }
2776                Schema::RecursiveRef { recursive_ref, .. } => {
2777                    // Array of recursive references
2778                    if recursive_ref == "#" {
2779                        // Self-reference to the current schema
2780                        let target = self
2781                            .find_recursive_anchor_schema()
2782                            .unwrap_or_else(|| parent_schema_name.to_string());
2783                        dependencies.insert(target.clone());
2784                        SchemaType::Reference { target }
2785                    } else {
2786                        let target = self
2787                            .extract_schema_name(recursive_ref)
2788                            .unwrap_or("RecursiveType")
2789                            .to_string();
2790                        dependencies.insert(target.clone());
2791                        SchemaType::Reference { target }
2792                    }
2793                }
2794                Schema::Typed { schema_type, .. } => {
2795                    // Array of primitive types
2796                    match schema_type {
2797                        OpenApiSchemaType::String => SchemaType::Primitive {
2798                            rust_type: "String".to_string(),
2799                        },
2800                        OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2801                            let details = items_schema.details();
2802                            let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2803                            SchemaType::Primitive { rust_type }
2804                        }
2805                        OpenApiSchemaType::Boolean => SchemaType::Primitive {
2806                            rust_type: "bool".to_string(),
2807                        },
2808                        OpenApiSchemaType::Object => {
2809                            // Inline object in array - create a named schema for it
2810                            let object_type_name = format!("{parent_schema_name}Item");
2811
2812                            // Analyze the object schema
2813                            let object_type =
2814                                self.analyze_object_schema(items_schema, dependencies)?;
2815
2816                            // Create an analyzed schema for the inline object
2817                            let inline_schema = AnalyzedSchema {
2818                                name: object_type_name.clone(),
2819                                original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
2820                                schema_type: object_type,
2821                                dependencies: dependencies.clone(),
2822                                nullable: false,
2823                                description: items_schema.details().description.clone(),
2824                                default: None,
2825                            };
2826
2827                            // Add the inline object as a named schema
2828                            self.resolved_cache
2829                                .insert(object_type_name.clone(), inline_schema);
2830                            dependencies.insert(object_type_name.clone());
2831
2832                            // Return a reference to the named schema
2833                            SchemaType::Reference {
2834                                target: object_type_name,
2835                            }
2836                        }
2837                        OpenApiSchemaType::Array => {
2838                            // Array of arrays - recursively analyze
2839                            self.analyze_array_schema(
2840                                items_schema,
2841                                parent_schema_name,
2842                                dependencies,
2843                            )?
2844                        }
2845                        _ => SchemaType::Primitive {
2846                            rust_type: "serde_json::Value".to_string(),
2847                        },
2848                    }
2849                }
2850                Schema::OneOf { .. } | Schema::AnyOf { .. } => {
2851                    // Union types in arrays - analyze recursively
2852                    let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
2853
2854                    // If we got a discriminated union or union, we need to create a separate schema for it
2855                    match &analyzed.schema_type {
2856                        SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
2857                            // Generate a unique name for the union schema based on the parent context
2858                            // Use the parent context directly to maintain consistent naming
2859                            let union_name = format!("{parent_schema_name}ItemUnion");
2860
2861                            // Create a new analyzed schema with the correct name
2862                            let mut union_schema = analyzed;
2863                            union_schema.name = union_name.clone();
2864
2865                            // Add the union as a separate schema
2866                            self.resolved_cache.insert(union_name.clone(), union_schema);
2867
2868                            // Add dependency
2869                            dependencies.insert(union_name.clone());
2870
2871                            // Return a reference to the union schema
2872                            SchemaType::Reference { target: union_name }
2873                        }
2874                        _ => analyzed.schema_type,
2875                    }
2876                }
2877                Schema::Untyped { .. } => {
2878                    // Try to infer the type
2879                    if let Some(inferred) = items_schema.inferred_type() {
2880                        match inferred {
2881                            OpenApiSchemaType::Object => {
2882                                // Inline object in array - create a named schema for it
2883                                let object_type_name = format!("{parent_schema_name}Item");
2884
2885                                // Analyze the object schema
2886                                let object_type =
2887                                    self.analyze_object_schema(items_schema, dependencies)?;
2888
2889                                // Create an analyzed schema for the inline object
2890                                let inline_schema = AnalyzedSchema {
2891                                    name: object_type_name.clone(),
2892                                    original: serde_json::to_value(items_schema)
2893                                        .unwrap_or(Value::Null),
2894                                    schema_type: object_type,
2895                                    dependencies: dependencies.clone(),
2896                                    nullable: false,
2897                                    description: items_schema.details().description.clone(),
2898                                    default: None,
2899                                };
2900
2901                                // Add the inline object as a named schema
2902                                self.resolved_cache
2903                                    .insert(object_type_name.clone(), inline_schema);
2904                                dependencies.insert(object_type_name.clone());
2905
2906                                // Return a reference to the named schema
2907                                SchemaType::Reference {
2908                                    target: object_type_name,
2909                                }
2910                            }
2911                            OpenApiSchemaType::String => SchemaType::Primitive {
2912                                rust_type: "String".to_string(),
2913                            },
2914                            OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2915                                let details = items_schema.details();
2916                                let rust_type = self.get_number_rust_type(inferred, details);
2917                                SchemaType::Primitive { rust_type }
2918                            }
2919                            OpenApiSchemaType::Boolean => SchemaType::Primitive {
2920                                rust_type: "bool".to_string(),
2921                            },
2922                            _ => SchemaType::Primitive {
2923                                rust_type: "serde_json::Value".to_string(),
2924                            },
2925                        }
2926                    } else {
2927                        SchemaType::Primitive {
2928                            rust_type: "serde_json::Value".to_string(),
2929                        }
2930                    }
2931                }
2932                _ => SchemaType::Primitive {
2933                    rust_type: "serde_json::Value".to_string(),
2934                },
2935            };
2936
2937            Ok(SchemaType::Array {
2938                item_type: Box::new(item_type),
2939            })
2940        } else {
2941            // No items specified, fall back to generic array
2942            Ok(SchemaType::Primitive {
2943                rust_type: "Vec<serde_json::Value>".to_string(),
2944            })
2945        }
2946    }
2947
2948    fn get_number_rust_type(
2949        &self,
2950        schema_type: OpenApiSchemaType,
2951        details: &crate::openapi::SchemaDetails,
2952    ) -> String {
2953        match schema_type {
2954            OpenApiSchemaType::Integer => {
2955                // Check format field for integer types
2956                match details.format.as_deref() {
2957                    Some("int32") => "i32".to_string(),
2958                    Some("int64") => "i64".to_string(),
2959                    _ => "i64".to_string(), // Default for integer
2960                }
2961            }
2962            OpenApiSchemaType::Number => {
2963                // Check format field for number types
2964                match details.format.as_deref() {
2965                    Some("float") => "f32".to_string(),
2966                    Some("double") => "f64".to_string(),
2967                    _ => "f64".to_string(), // Default for number
2968                }
2969            }
2970            _ => "serde_json::Value".to_string(), // Fallback
2971        }
2972    }
2973
2974    fn analyze_anyof_union(
2975        &mut self,
2976        any_of_schemas: &[Schema],
2977        discriminator: Option<&Discriminator>,
2978        dependencies: &mut HashSet<String>,
2979        context_name: &str,
2980    ) -> Result<SchemaType> {
2981        // Analyze the semantics of this anyOf
2982
2983        // Pattern 1: Nullable type [Type, null]
2984        if any_of_schemas.len() == 2 {
2985            let null_count = any_of_schemas
2986                .iter()
2987                .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2988                .count();
2989            if null_count == 1 {
2990                // This is a nullable pattern - find the non-null type
2991                for schema in any_of_schemas {
2992                    if !matches!(schema.schema_type(), Some(OpenApiSchemaType::Null)) {
2993                        // For nullable pattern, return the non-null type directly
2994                        // The nullable information is handled at the property level
2995                        return self
2996                            .analyze_schema_value(schema, context_name)
2997                            .map(|a| a.schema_type);
2998                    }
2999                }
3000            }
3001        }
3002
3003        // Pattern 2: Multiple complex types or mixed primitive/complex = flexible union
3004        let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3005        let has_objects = any_of_schemas.iter().any(|s| {
3006            matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3007                || s.inferred_type() == Some(OpenApiSchemaType::Object)
3008        });
3009        let has_arrays = any_of_schemas
3010            .iter()
3011            .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3012
3013        // Handle mixed primitive and complex types (like string + array of objects)
3014        // Skip this pattern if all schemas are strings or const values (handle in pattern 3)
3015        let all_string_like = any_of_schemas.iter().all(|s| {
3016            matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3017                || s.details().const_value.is_some()
3018        });
3019
3020        if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3021            // Check if this is a discriminated union
3022            if let Some(disc) = discriminator {
3023                // This is a discriminated anyOf union, analyze it the same way as oneOf
3024                return self.analyze_oneof_union(any_of_schemas, Some(disc), None, dependencies);
3025            }
3026
3027            // Auto-detect implicit discriminator from const fields across all variants
3028            if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3029                return self.analyze_oneof_union(
3030                    any_of_schemas,
3031                    Some(&Discriminator {
3032                        property_name: disc_field,
3033                        mapping: None,
3034                        extra: BTreeMap::new(),
3035                    }),
3036                    None,
3037                    dependencies,
3038                );
3039            }
3040
3041            // Create an untagged union for flexible matching
3042            let mut variants = Vec::new();
3043
3044            for schema in any_of_schemas {
3045                if let Some(ref_str) = schema.reference() {
3046                    if let Some(target) = self.extract_schema_name(ref_str) {
3047                        dependencies.insert(target.to_string());
3048                        variants.push(SchemaRef {
3049                            target: target.to_string(),
3050                            nullable: false,
3051                        });
3052                    }
3053                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3054                    || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3055                {
3056                    // Generate inline object type for anyOf union
3057                    let inline_index = variants.len();
3058                    let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3059
3060                    // Store inline schema for later analysis and generation
3061                    self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3062
3063                    variants.push(SchemaRef {
3064                        target: inline_type_name,
3065                        nullable: false,
3066                    });
3067                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3068                    // Handle array types in unions by creating a type alias
3069                    let array_type =
3070                        self.analyze_array_schema(schema, context_name, dependencies)?;
3071
3072                    // Create a unique name for this array type in the union
3073                    let array_type_name = if let Some(items_schema) = &schema.details().items {
3074                        if let Some(ref_str) = items_schema.reference() {
3075                            if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3076                                dependencies.insert(item_type_name.to_string());
3077                                format!("{item_type_name}Array")
3078                            } else {
3079                                self.generate_context_aware_name(
3080                                    context_name,
3081                                    "Array",
3082                                    variants.len(),
3083                                    Some(schema),
3084                                )
3085                            }
3086                        } else {
3087                            self.generate_context_aware_name(
3088                                context_name,
3089                                "Array",
3090                                variants.len(),
3091                                Some(schema),
3092                            )
3093                        }
3094                    } else {
3095                        self.generate_context_aware_name(
3096                            context_name,
3097                            "Array",
3098                            variants.len(),
3099                            Some(schema),
3100                        )
3101                    };
3102
3103                    // Store the array as a type alias
3104                    self.resolved_cache.insert(
3105                        array_type_name.clone(),
3106                        AnalyzedSchema {
3107                            name: array_type_name.clone(),
3108                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
3109                            schema_type: array_type,
3110                            dependencies: HashSet::new(),
3111                            nullable: false,
3112                            description: Some("Array variant in union".to_string()),
3113                            default: None,
3114                        },
3115                    );
3116
3117                    // Add array type as a dependency
3118                    dependencies.insert(array_type_name.clone());
3119
3120                    variants.push(SchemaRef {
3121                        target: array_type_name,
3122                        nullable: false,
3123                    });
3124                } else if let Some(schema_type) = schema.schema_type() {
3125                    // Handle primitive types by creating type aliases for consistency
3126                    let inline_index = variants.len();
3127
3128                    // Generate a better name for primitive types
3129                    let inline_type_name = match schema_type {
3130                        OpenApiSchemaType::String => {
3131                            // For string types, check if we can infer a better name from context
3132                            // If this is the first variant and it's a string, use a simple name
3133                            if inline_index == 0 {
3134                                format!("{context_name}String")
3135                            } else {
3136                                format!("{context_name}StringVariant{inline_index}")
3137                            }
3138                        }
3139                        OpenApiSchemaType::Number => {
3140                            if inline_index == 0 {
3141                                format!("{context_name}Number")
3142                            } else {
3143                                format!("{context_name}NumberVariant{inline_index}")
3144                            }
3145                        }
3146                        OpenApiSchemaType::Integer => {
3147                            if inline_index == 0 {
3148                                format!("{context_name}Integer")
3149                            } else {
3150                                format!("{context_name}IntegerVariant{inline_index}")
3151                            }
3152                        }
3153                        OpenApiSchemaType::Boolean => {
3154                            if inline_index == 0 {
3155                                format!("{context_name}Boolean")
3156                            } else {
3157                                format!("{context_name}BooleanVariant{inline_index}")
3158                            }
3159                        }
3160                        _ => format!("{context_name}Variant{inline_index}"),
3161                    };
3162
3163                    let rust_type =
3164                        self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3165
3166                    // Store as a type alias
3167                    self.resolved_cache.insert(
3168                        inline_type_name.clone(),
3169                        AnalyzedSchema {
3170                            name: inline_type_name.clone(),
3171                            original: serde_json::to_value(schema).unwrap_or(Value::Null),
3172                            schema_type: SchemaType::Primitive { rust_type },
3173                            dependencies: HashSet::new(),
3174                            nullable: false,
3175                            description: schema.details().description.clone(),
3176                            default: None,
3177                        },
3178                    );
3179
3180                    // Add inline type as a dependency
3181                    dependencies.insert(inline_type_name.clone());
3182
3183                    variants.push(SchemaRef {
3184                        target: inline_type_name,
3185                        nullable: false,
3186                    });
3187                }
3188            }
3189
3190            if !variants.is_empty() {
3191                return Ok(SchemaType::Union { variants });
3192            }
3193        }
3194
3195        // Pattern 3: String enum pattern (mix of "type": "string" and const values)
3196        let all_strings = any_of_schemas.iter().all(|schema| {
3197            matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3198                || schema.details().const_value.is_some()
3199        });
3200
3201        if all_strings {
3202            // Collect all constant values as enum variants
3203            let mut enum_values = Vec::new();
3204            let mut has_open_string = false;
3205
3206            for schema in any_of_schemas {
3207                if let Some(const_val) = &schema.details().const_value {
3208                    if let Some(const_str) = const_val.as_str() {
3209                        enum_values.push(const_str.to_string());
3210                    }
3211                } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3212                    has_open_string = true;
3213                }
3214            }
3215
3216            if !enum_values.is_empty() {
3217                if has_open_string {
3218                    // Has both constants and open string - create an extensible enum
3219                    // This generates an enum with known variants plus a Custom(String) variant
3220                    return Ok(SchemaType::ExtensibleEnum {
3221                        known_values: enum_values,
3222                    });
3223                } else {
3224                    // All constants - create string enum
3225                    return Ok(SchemaType::StringEnum {
3226                        values: enum_values,
3227                    });
3228                }
3229            }
3230        }
3231
3232        // Pattern 4: Mixed primitives = fall back to serde_json::Value
3233        Ok(SchemaType::Primitive {
3234            rust_type: "serde_json::Value".to_string(),
3235        })
3236    }
3237
3238    /// Find the schema with $recursiveAnchor: true for resolving $recursiveRef: "#"
3239    fn find_recursive_anchor_schema(&self) -> Option<String> {
3240        // Search through all schemas to find one with $recursiveAnchor: true
3241        for (schema_name, schema) in &self.schemas {
3242            let details = schema.details();
3243            if details.recursive_anchor == Some(true) {
3244                return Some(schema_name.clone());
3245            }
3246        }
3247
3248        // If no schema has $recursiveAnchor: true, this might be an older spec
3249        // In that case, $recursiveRef: "#" typically refers to the root schema
3250        // For now, return None to indicate we couldn't resolve it
3251        None
3252    }
3253
3254    /// Detect if a schema should use serde_json::Value for dynamic JSON
3255    /// Based on structural patterns identified in real-world APIs
3256    fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3257        // Pattern 1: anyOf with [object, null] where object has no properties
3258        if let Schema::AnyOf { any_of, .. } = schema {
3259            if any_of.len() == 2 {
3260                let has_null = any_of
3261                    .iter()
3262                    .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3263                let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3264
3265                if has_null && has_empty_object {
3266                    return true;
3267                }
3268            }
3269        }
3270
3271        // Pattern 2: Direct empty object pattern
3272        self.is_dynamic_object_pattern(schema)
3273    }
3274
3275    /// Check if a schema represents a dynamic object pattern
3276    fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3277        // Must be object type or untyped with object inference
3278        let is_object = match schema.schema_type() {
3279            Some(OpenApiSchemaType::Object) => true,
3280            None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3281            _ => false,
3282        };
3283
3284        if !is_object {
3285            return false;
3286        }
3287
3288        let details = schema.details();
3289
3290        // If it has explicit additionalProperties, it should remain as a typed object
3291        // that will be generated as BTreeMap<String, serde_json::Value> or similar
3292        if self.has_explicit_additional_properties(schema) {
3293            return false;
3294        }
3295
3296        // Pattern 1: Object with no properties at all (and no additionalProperties)
3297        let no_properties = details
3298            .properties
3299            .as_ref()
3300            .map(|props| props.is_empty())
3301            .unwrap_or(true);
3302
3303        if no_properties {
3304            // Check for constraints that would make this a structured type
3305            let has_structural_constraints =
3306                // Has required fields (other than just 'type')
3307                details.required.as_ref()
3308                    .map(|req| req.iter().any(|r| r != "type"))
3309                    .unwrap_or(false)
3310                // Has pattern-based property definitions    
3311                || details.extra.contains_key("patternProperties")
3312                // Has property name schema
3313                || details.extra.contains_key("propertyNames")
3314                // Has min/max property constraints
3315                || details.extra.contains_key("minProperties")
3316                || details.extra.contains_key("maxProperties")
3317                // Has specific property dependencies
3318                || details.extra.contains_key("dependencies")
3319                // Has conditional schemas
3320                || details.extra.contains_key("if")
3321                || details.extra.contains_key("then")
3322                || details.extra.contains_key("else");
3323
3324            return !has_structural_constraints;
3325        }
3326
3327        false
3328    }
3329
3330    /// Check if this is an object that explicitly allows arbitrary additional properties
3331    fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
3332        let details = schema.details();
3333
3334        // Check if additionalProperties is explicitly set to true or a schema
3335        matches!(
3336            &details.additional_properties,
3337            Some(crate::openapi::AdditionalProperties::Boolean(true))
3338                | Some(crate::openapi::AdditionalProperties::Schema(_))
3339        )
3340    }
3341
3342    /// Analyze OpenAPI operations to extract request/response schemas
3343    fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
3344        let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
3345            .map_err(GeneratorError::ParseError)?;
3346
3347        if let Some(paths) = &spec.paths {
3348            for (path, path_item) in paths {
3349                for (method, operation) in path_item.operations() {
3350                    // Generate operation ID if missing
3351                    let operation_id = operation
3352                        .operation_id
3353                        .clone()
3354                        .unwrap_or_else(|| Self::generate_operation_id(method, path));
3355
3356                    let op_info = self.analyze_single_operation(
3357                        &operation_id,
3358                        method,
3359                        path,
3360                        operation,
3361                        analysis,
3362                    )?;
3363                    analysis.operations.insert(operation_id, op_info);
3364                }
3365            }
3366        }
3367        Ok(())
3368    }
3369
3370    /// Generate an operation ID from method and path when not provided
3371    /// Converts paths like "/v0/servers/{serverId}" + "get" to "getV0ServersServerId"
3372    fn generate_operation_id(method: &str, path: &str) -> String {
3373        // Start with the HTTP method in lowercase
3374        let mut operation_id = method.to_lowercase();
3375
3376        // Process the path: remove leading slash, split by /, convert to camelCase
3377        let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
3378
3379        for part in path_parts {
3380            if part.is_empty() {
3381                continue;
3382            }
3383
3384            // Handle path parameters: {serverId} -> ServerId
3385            let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
3386                &part[1..part.len() - 1]
3387            } else {
3388                part
3389            };
3390
3391            // Convert to PascalCase and append
3392            let pascal_case_part = cleaned_part
3393                .split(&['-', '_'][..])
3394                .map(|s| {
3395                    let mut chars = s.chars();
3396                    match chars.next() {
3397                        None => String::new(),
3398                        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3399                    }
3400                })
3401                .collect::<String>();
3402
3403            operation_id.push_str(&pascal_case_part);
3404        }
3405
3406        operation_id
3407    }
3408
3409    /// Analyze a single OpenAPI operation
3410    fn analyze_single_operation(
3411        &mut self,
3412        operation_id: &str,
3413        method: &str,
3414        path: &str,
3415        operation: &crate::openapi::Operation,
3416        _analysis: &mut SchemaAnalysis,
3417    ) -> Result<OperationInfo> {
3418        let mut op_info = OperationInfo {
3419            operation_id: operation_id.to_string(),
3420            method: method.to_uppercase(),
3421            path: path.to_string(),
3422            request_body: None,
3423            response_schemas: BTreeMap::new(),
3424            parameters: Vec::new(),
3425            supports_streaming: false, // Will be determined by StreamingConfig, not spec
3426            stream_parameter: None,    // Will be determined by StreamingConfig, not spec
3427        };
3428
3429        // Extract request body schema with content-type awareness
3430        if let Some(request_body) = &operation.request_body
3431            && let Some((content_type, maybe_schema)) = request_body.best_content()
3432        {
3433            op_info.request_body = match content_type {
3434                "application/json" => maybe_schema
3435                    .map(|s| {
3436                        self.resolve_or_inline_schema(s, operation_id, "Request")
3437                            .map(|name| RequestBodyContent::Json { schema_name: name })
3438                    })
3439                    .transpose()?,
3440                "application/x-www-form-urlencoded" => maybe_schema
3441                    .map(|s| {
3442                        self.resolve_or_inline_schema(s, operation_id, "Request")
3443                            .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
3444                    })
3445                    .transpose()?,
3446                "multipart/form-data" => Some(RequestBodyContent::Multipart),
3447                "application/octet-stream" => Some(RequestBodyContent::OctetStream),
3448                "text/plain" => Some(RequestBodyContent::TextPlain),
3449                _ => None,
3450            };
3451        }
3452
3453        // Extract response schemas
3454        if let Some(responses) = &operation.responses {
3455            for (status_code, response) in responses {
3456                if let Some(schema) = response.json_schema() {
3457                    if let Some(schema_ref) = schema.reference() {
3458                        // Named schema reference
3459                        if let Some(schema_name) = self.extract_schema_name(schema_ref) {
3460                            op_info
3461                                .response_schemas
3462                                .insert(status_code.clone(), schema_name.to_string());
3463                        }
3464                    } else {
3465                        // Inline schema - generate a synthetic type name and analyze it
3466                        let synthetic_name =
3467                            self.generate_inline_response_type_name(operation_id, status_code);
3468
3469                        // Use the existing inline schema infrastructure
3470                        let mut deps = HashSet::new();
3471                        self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3472
3473                        op_info
3474                            .response_schemas
3475                            .insert(status_code.clone(), synthetic_name);
3476                    }
3477                }
3478            }
3479        }
3480
3481        // Extract parameters
3482        if let Some(parameters) = &operation.parameters {
3483            for param in parameters {
3484                if let Some(param_info) = self.analyze_parameter(param)? {
3485                    op_info.parameters.push(param_info);
3486                }
3487            }
3488        }
3489
3490        Ok(op_info)
3491    }
3492
3493    /// Generate a type name for an inline response schema
3494    fn generate_inline_response_type_name(&self, operation_id: &str, _status_code: &str) -> String {
3495        use heck::ToPascalCase;
3496        // Convert operation_id to PascalCase and append Response
3497        // e.g., "app.skills" -> "AppSkillsResponse"
3498        // e.g., "getUser" + "200" -> "GetUserResponse"
3499        let base_name = operation_id.replace('.', "_").to_pascal_case();
3500        format!("{}Response", base_name)
3501    }
3502
3503    /// Generate a type name for an inline request body schema
3504    fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
3505        use heck::ToPascalCase;
3506        // Convert operation_id to PascalCase and append Request
3507        // e.g., "session.prompt" -> "SessionPromptRequest"
3508        // e.g., "pty.create" -> "PtyCreateRequest"
3509        let base_name = operation_id.replace('.', "_").to_pascal_case();
3510        format!("{}Request", base_name)
3511    }
3512
3513    /// Resolve a schema reference to a name, or inline it with a synthetic name.
3514    /// `suffix` controls the generated name (e.g. "Request" or "Response").
3515    fn resolve_or_inline_schema(
3516        &mut self,
3517        schema: &crate::openapi::Schema,
3518        operation_id: &str,
3519        suffix: &str,
3520    ) -> Result<String> {
3521        if let Some(schema_ref) = schema.reference()
3522            && let Some(schema_name) = self.extract_schema_name(schema_ref)
3523        {
3524            return Ok(schema_name.to_string());
3525        }
3526        // Inline schema - generate a synthetic type name and analyze it
3527        let synthetic_name = if suffix == "Request" {
3528            self.generate_inline_request_type_name(operation_id)
3529        } else {
3530            self.generate_inline_response_type_name(operation_id, "")
3531        };
3532        let mut deps = HashSet::new();
3533        self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
3534        Ok(synthetic_name)
3535    }
3536
3537    /// Analyze a parameter
3538    fn analyze_parameter(
3539        &self,
3540        param: &crate::openapi::Parameter,
3541    ) -> Result<Option<ParameterInfo>> {
3542        let name = param.name.as_deref().unwrap_or("");
3543        let location = param.location.as_deref().unwrap_or("");
3544        let required = param.required.unwrap_or(false);
3545
3546        let mut rust_type = "String".to_string();
3547        let mut schema_ref = None;
3548
3549        if let Some(schema) = &param.schema {
3550            if let Some(ref_str) = schema.reference() {
3551                schema_ref = self.extract_schema_name(ref_str).map(|s| s.to_string());
3552            } else if let Some(schema_type) = schema.schema_type() {
3553                rust_type = match schema_type {
3554                    crate::openapi::SchemaType::Boolean => "bool",
3555                    crate::openapi::SchemaType::Integer => "i64",
3556                    crate::openapi::SchemaType::Number => "f64",
3557                    crate::openapi::SchemaType::String => "String",
3558                    _ => "String",
3559                }
3560                .to_string();
3561            }
3562        }
3563
3564        Ok(Some(ParameterInfo {
3565            name: name.to_string(),
3566            location: location.to_string(),
3567            required,
3568            schema_ref,
3569            rust_type,
3570        }))
3571    }
3572}