vika_cli/generator/
zod_schema.rs

1use crate::error::Result;
2use crate::generator::swagger_parser::{get_schema_name_from_ref, resolve_ref};
3use crate::generator::utils::{sanitize_property_name, to_pascal_case};
4use crate::templates::context::ZodContext;
5use crate::templates::engine::TemplateEngine;
6use crate::templates::registry::TemplateId;
7use openapiv3::{OpenAPI, ReferenceOr, Schema, SchemaKind, Type};
8use std::collections::HashMap;
9
10pub struct ZodSchema {
11    pub content: String,
12}
13
14pub fn generate_zod_schemas(
15    openapi: &OpenAPI,
16    schemas: &HashMap<String, Schema>,
17    schema_names: &[String],
18) -> Result<Vec<ZodSchema>> {
19    generate_zod_schemas_with_registry(
20        openapi,
21        schemas,
22        schema_names,
23        &mut std::collections::HashMap::new(),
24        &[],
25    )
26}
27
28pub fn generate_zod_schemas_with_registry(
29    openapi: &OpenAPI,
30    schemas: &HashMap<String, Schema>,
31    schema_names: &[String],
32    enum_registry: &mut std::collections::HashMap<String, String>,
33    common_schemas: &[String],
34) -> Result<Vec<ZodSchema>> {
35    generate_zod_schemas_with_registry_and_engine(
36        openapi,
37        schemas,
38        schema_names,
39        enum_registry,
40        common_schemas,
41        None,
42    )
43}
44
45pub fn generate_zod_schemas_with_registry_and_engine(
46    openapi: &OpenAPI,
47    schemas: &HashMap<String, Schema>,
48    schema_names: &[String],
49    enum_registry: &mut std::collections::HashMap<String, String>,
50    common_schemas: &[String],
51    template_engine: Option<&TemplateEngine>,
52) -> Result<Vec<ZodSchema>> {
53    generate_zod_schemas_with_registry_and_engine_and_spec(
54        openapi,
55        schemas,
56        schema_names,
57        enum_registry,
58        common_schemas,
59        template_engine,
60        None,
61    )
62}
63
64pub fn generate_zod_schemas_with_registry_and_engine_and_spec(
65    openapi: &OpenAPI,
66    schemas: &HashMap<String, Schema>,
67    schema_names: &[String],
68    enum_registry: &mut std::collections::HashMap<String, String>,
69    common_schemas: &[String],
70    template_engine: Option<&TemplateEngine>,
71    spec_name: Option<&str>,
72) -> Result<Vec<ZodSchema>> {
73    let mut zod_schemas = Vec::new();
74    let mut processed = std::collections::HashSet::new();
75
76    for schema_name in schema_names {
77        if let Some(schema) = schemas.get(schema_name) {
78            generate_zod_for_schema(
79                openapi,
80                schema_name,
81                schema,
82                &mut zod_schemas,
83                &mut processed,
84                enum_registry,
85                None,
86                common_schemas,
87                template_engine,
88                spec_name,
89            )?;
90        }
91    }
92
93    Ok(zod_schemas)
94}
95
96#[allow(clippy::too_many_arguments)]
97fn generate_zod_for_schema(
98    openapi: &OpenAPI,
99    name: &str,
100    schema: &Schema,
101    zod_schemas: &mut Vec<ZodSchema>,
102    processed: &mut std::collections::HashSet<String>,
103    enum_registry: &mut std::collections::HashMap<String, String>,
104    parent_schema_name: Option<&str>,
105    common_schemas: &[String],
106    template_engine: Option<&TemplateEngine>,
107    spec_name: Option<&str>,
108) -> Result<()> {
109    if processed.contains(name) {
110        return Ok(());
111    }
112    processed.insert(name.to_string());
113
114    let schema_name = to_pascal_case(name);
115    let zod_def = schema_to_zod(
116        openapi,
117        schema,
118        zod_schemas,
119        processed,
120        0,
121        enum_registry,
122        None,
123        parent_schema_name,
124        common_schemas,
125        template_engine,
126        spec_name,
127    )?;
128
129    // Handle enums at top level (when schema itself is an enum)
130    // Note: For property-level enums, they're handled in schema_to_zod with context
131    if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
132        if !string_type.enumeration.is_empty() {
133            let mut enum_values: Vec<String> = string_type
134                .enumeration
135                .iter()
136                .filter_map(|v| v.as_ref().cloned())
137                .collect();
138            if !enum_values.is_empty() {
139                enum_values.sort();
140                let enum_key = enum_values.join(",");
141                let schema_context_key = format!("schema_enum:{}", name);
142
143                // Use schema name if available, otherwise generate from values
144                let enum_name = if !name.is_empty() {
145                    format!("{}Enum", to_pascal_case(name))
146                } else {
147                    // Generate unique name from values
148                    let value_hash: String = enum_values
149                        .iter()
150                        .take(3)
151                        .map(|v| v.chars().next().unwrap_or('X'))
152                        .collect();
153                    format!("Enum{}", value_hash)
154                };
155
156                // Check if already generated in THIS zod_schemas collection
157                let schema_export = format!("export const {}Schema", enum_name);
158                let already_generated = zod_schemas
159                    .iter()
160                    .any(|s| s.content.contains(&schema_export));
161
162                if !already_generated {
163                    // Store in registry (schema-specific + base key for reuse)
164                    enum_registry.insert(schema_context_key, enum_name.clone());
165                    if !enum_registry.contains_key(&enum_key) {
166                        enum_registry.insert(enum_key.clone(), enum_name.clone());
167                    }
168                    if !name.is_empty() {
169                        enum_registry.insert(format!("schema:{}", name), enum_name.clone());
170                    }
171
172                    if let Some(engine) = template_engine {
173                        let context = ZodContext::enum_schema(
174                            enum_name.clone(),
175                            enum_values.clone(),
176                            spec_name.map(|s| s.to_string()),
177                        );
178                        let content = engine.render(TemplateId::ZodEnum, &context)?;
179                        zod_schemas.push(ZodSchema { content });
180                    } else {
181                        let enum_schema = generate_enum_zod(&enum_name, &enum_values);
182                        zod_schemas.push(enum_schema);
183                    }
184                }
185                return Ok(());
186            }
187        }
188    }
189
190    // Only create object schema if it's an object type
191    if matches!(&schema.schema_kind, SchemaKind::Type(Type::Object(_))) {
192        // Check if this is an empty object (should be a record type)
193        if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
194            if obj.properties.is_empty() {
195                // Empty object with additionalProperties - use z.record() directly
196                if let Some(engine) = template_engine {
197                    let description = schema.schema_data.description.clone();
198                    let context = ZodContext::schema_with_annotation(
199                        schema_name.clone(),
200                        "z.record(z.string(), z.any())".to_string(),
201                        description,
202                        spec_name.map(|s| s.to_string()),
203                    );
204                    let content = engine.render(TemplateId::ZodSchema, &context)?;
205                    zod_schemas.push(ZodSchema { content });
206                } else {
207                    zod_schemas.push(ZodSchema {
208                        content: format!(
209                            "export const {}Schema: z.ZodType<any> = z.record(z.string(), z.any());",
210                            schema_name
211                        ),
212                    });
213                }
214            } else {
215                // Regular object with properties
216                // Check if this schema has circular references (if it's already processed, it might be circular)
217                let has_circular_ref = zod_def.contains("z.lazy");
218                if let Some(engine) = template_engine {
219                    let description = schema.schema_data.description.clone();
220                    let zod_expr = format!("z.object({{\n{}\n}})", zod_def);
221                    let context = if has_circular_ref {
222                        ZodContext::schema_with_annotation(
223                            schema_name.clone(),
224                            zod_expr,
225                            description,
226                            spec_name.map(|s| s.to_string()),
227                        )
228                    } else {
229                        ZodContext::schema(
230                            schema_name.clone(),
231                            zod_expr,
232                            description,
233                            spec_name.map(|s| s.to_string()),
234                        )
235                    };
236                    let content = engine.render(TemplateId::ZodSchema, &context)?;
237                    zod_schemas.push(ZodSchema { content });
238                } else {
239                    zod_schemas.push(ZodSchema {
240                        content: format!(
241                            "export const {}Schema: z.ZodType<any> = z.object({{\n{}\n}});",
242                            schema_name, zod_def
243                        ),
244                    });
245                }
246            }
247        }
248    }
249
250    Ok(())
251}
252
253#[allow(clippy::too_many_arguments)]
254fn schema_to_zod(
255    openapi: &OpenAPI,
256    schema: &Schema,
257    zod_schemas: &mut Vec<ZodSchema>,
258    processed: &mut std::collections::HashSet<String>,
259    indent: usize,
260    enum_registry: &mut std::collections::HashMap<String, String>,
261    context: Option<(&str, &str)>, // (property_name, parent_schema_name)
262    current_schema_name: Option<&str>, // Current schema being processed (for enum naming context)
263    common_schemas: &[String],
264    template_engine: Option<&TemplateEngine>,
265    spec_name: Option<&str>,
266) -> Result<String> {
267    // Prevent infinite recursion with a reasonable depth limit
268    if indent > 100 {
269        return Ok(format!("{}z.any()", "  ".repeat(indent)));
270    }
271    let indent_str = "  ".repeat(indent);
272
273    match &schema.schema_kind {
274        SchemaKind::Type(type_) => {
275            match type_ {
276                Type::String(string_type) => {
277                    if !string_type.enumeration.is_empty() {
278                        let mut enum_values: Vec<String> = string_type
279                            .enumeration
280                            .iter()
281                            .filter_map(|v| v.as_ref().cloned())
282                            .collect();
283                        if !enum_values.is_empty() {
284                            // Create a key from sorted enum values to check registry
285                            enum_values.sort();
286                            let enum_key = enum_values.join(",");
287
288                            // For generic property names, include context in the key to avoid conflicts
289                            // Only do this for truly generic names like status/type/state/kind
290                            // For other generic identifiers (key/code/id/name), let them reuse based on values
291                            let context_key = if let Some((prop_name, parent_schema)) = context {
292                                let generic_names = ["status", "type", "state", "kind"];
293                                if generic_names.contains(&prop_name.to_lowercase().as_str())
294                                    && !parent_schema.is_empty()
295                                {
296                                    // Include parent schema in key for generic properties to avoid conflicts
297                                    format!("{}:{}", enum_key, parent_schema)
298                                } else {
299                                    enum_key.clone()
300                                }
301                            } else {
302                                enum_key.clone()
303                            };
304
305                            // Check if this enum already exists in registry
306                            // First check context_key (for context-aware enums)
307                            // Then check base enum_key to deduplicate enums with same values
308                            // This ensures enums with the same values reuse the same enum name
309                            let existing_enum_name = enum_registry
310                                .get(&context_key)
311                                .or_else(|| enum_registry.get(&enum_key))
312                                .cloned();
313
314                            if let Some(existing_name) = existing_enum_name {
315                                // Store in registry with context_key for future lookups
316                                enum_registry.insert(context_key.clone(), existing_name.clone());
317
318                                // Check if enum schema has already been generated
319                                // Must match the actual definition, not just a reference
320                                let schema_already_generated = zod_schemas.iter().any(|s| {
321                                    let schema_name_pattern = format!("{}Schema", existing_name);
322                                    s.content.contains(&format!(
323                                        "export const {} =",
324                                        schema_name_pattern
325                                    ))
326                                });
327
328                                // If not generated yet, generate it now
329                                if !schema_already_generated {
330                                    if let Some(engine) = template_engine {
331                                        let context = ZodContext::enum_schema(
332                                            existing_name.clone(),
333                                            enum_values.clone(),
334                                            spec_name.map(|s| s.to_string()),
335                                        );
336                                        let content =
337                                            engine.render(TemplateId::ZodEnum, &context)?;
338                                        zod_schemas.push(ZodSchema { content });
339                                    } else {
340                                        let enum_schema =
341                                            generate_enum_zod(&existing_name, &enum_values);
342                                        zod_schemas.push(enum_schema);
343                                    }
344                                }
345
346                                // Enums don't need z.lazy(), use directly
347                                return Ok(format!("{}{}Schema", indent_str, existing_name));
348                            }
349
350                            // Generate meaningful enum name using context (property name + parent schema) or fallback
351                            let enum_name = if let Some((prop_name, parent_schema)) = context {
352                                // Use property name + parent schema for meaningful name to avoid conflicts
353                                // For generic names like "status", use parent schema to differentiate
354                                let prop_pascal = to_pascal_case(prop_name);
355
356                                // If property name is generic (status, type, etc.), use parent schema
357                                let generic_names = ["status", "type", "state", "kind"];
358                                if generic_names.contains(&prop_name.to_lowercase().as_str())
359                                    && !parent_schema.is_empty()
360                                {
361                                    let parent_pascal = to_pascal_case(parent_schema);
362                                    // Remove common suffixes from parent schema name
363                                    let parent_clean = parent_pascal
364                                        .trim_end_matches("ResponseDto")
365                                        .trim_end_matches("Dto")
366                                        .trim_end_matches("Response")
367                                        .to_string();
368
369                                    // Check if parent already contains the property name (e.g., "KycStatus" contains "Status")
370                                    // Use case-insensitive matching and check if property name is a suffix or contained
371                                    let prop_lower = prop_pascal.to_lowercase();
372                                    let parent_lower = parent_clean.to_lowercase();
373
374                                    // Check if parent ends with property name (e.g., "KycStatus" ends with "Status")
375                                    // or if property is contained in parent (case-insensitive)
376                                    if parent_lower.ends_with(&prop_lower)
377                                        || parent_lower.contains(&prop_lower)
378                                    {
379                                        // Parent already contains property name, just use parent + Enum
380                                        format!("{}Enum", parent_clean)
381                                    } else {
382                                        // Combine parent + property
383                                        format!("{}{}Enum", parent_clean, prop_pascal)
384                                    }
385                                } else {
386                                    // For non-generic property names, check if parent schema name suggests a better enum name
387                                    // This helps with cases like "key" and "provider" in "AvailableProviderDto" both referring to provider enums
388                                    if !parent_schema.is_empty() {
389                                        let parent_pascal = to_pascal_case(parent_schema);
390                                        let parent_clean = parent_pascal
391                                            .trim_end_matches("ResponseDto")
392                                            .trim_end_matches("Dto")
393                                            .trim_end_matches("Response")
394                                            .to_string();
395
396                                        // If parent contains a word that matches the property conceptually, use that
397                                        // For example, "AvailableProviderDto" contains "Provider", so "key" property should use "ProviderEnum"
398
399                                        // Check if parent contains a more descriptive word (like "Provider" in "AvailableProvider")
400                                        // and the property is a generic identifier (like "key", "id", "name", "code")
401                                        let generic_identifiers =
402                                            ["key", "id", "name", "value", "code"];
403                                        if generic_identifiers
404                                            .contains(&prop_name.to_lowercase().as_str())
405                                        {
406                                            // For generic identifiers, always use parent schema to create unique enum names
407                                            // This prevents conflicts when different schemas have the same property name
408                                            // (e.g., CurrencyResponseDto.code vs OrderValidationErrorDto.code)
409                                            let meaningful_part = parent_clean
410                                                .trim_start_matches("Available")
411                                                .trim_start_matches("Get")
412                                                .trim_start_matches("Create")
413                                                .trim_start_matches("Update")
414                                                .trim_start_matches("Delete");
415
416                                            // Always include parent schema name for generic identifiers to avoid conflicts
417                                            if !meaningful_part.is_empty() {
418                                                format!("{}{}Enum", meaningful_part, prop_pascal)
419                                            } else {
420                                                format!("{}{}Enum", parent_clean, prop_pascal)
421                                            }
422                                        } else {
423                                            format!("{}Enum", prop_pascal)
424                                        }
425                                    } else {
426                                        format!("{}Enum", prop_pascal)
427                                    }
428                                }
429                            } else if !enum_values.is_empty() {
430                                // Fallback: use first value to create name
431                                let first_value = &enum_values[0];
432                                let base_name = first_value
433                                    .chars()
434                                    .take(1)
435                                    .collect::<String>()
436                                    .to_uppercase()
437                                    + &first_value.chars().skip(1).collect::<String>();
438                                format!("{}Enum", to_pascal_case(&base_name))
439                            } else {
440                                "UnknownEnum".to_string()
441                            };
442
443                            // Store in registry using context_key (includes context for generic properties)
444                            // Also store with base enum_key for deduplication
445                            enum_registry.insert(context_key.clone(), enum_name.clone());
446                            enum_registry.insert(enum_key.clone(), enum_name.clone());
447
448                            // Generate enum schema
449                            if let Some(engine) = template_engine {
450                                let context = ZodContext::enum_schema(
451                                    enum_name.clone(),
452                                    enum_values.clone(),
453                                    spec_name.map(|s| s.to_string()),
454                                );
455                                let content = engine.render(TemplateId::ZodEnum, &context)?;
456                                zod_schemas.push(ZodSchema { content });
457                            } else {
458                                let enum_schema = generate_enum_zod(&enum_name, &enum_values);
459                                zod_schemas.push(enum_schema);
460                            }
461
462                            // Enums don't need z.lazy(), use directly
463                            return Ok(format!("{}{}Schema", indent_str, enum_name));
464                        }
465                    }
466                    let mut zod_expr = format!("{}z.string()", indent_str);
467
468                    // Add string constraints
469                    if let Some(min_length) = string_type.min_length {
470                        zod_expr = format!("{}.min({})", zod_expr, min_length);
471                    }
472                    if let Some(max_length) = string_type.max_length {
473                        zod_expr = format!("{}.max({})", zod_expr, max_length);
474                    }
475                    if let Some(pattern) = &string_type.pattern {
476                        // Escape regex pattern for JavaScript
477                        let escaped_pattern = pattern.replace('\\', "\\\\").replace('"', "\\\"");
478                        zod_expr = format!("{}.regex(/^{}$/)", zod_expr, escaped_pattern);
479                    }
480                    // Handle format - it's a VariantOrUnknownOrEmpty
481                    // Try to extract format as string from both Item and Unknown variants
482                    let format_str_opt = match &string_type.format {
483                        openapiv3::VariantOrUnknownOrEmpty::Item(format) => {
484                            // Convert enum variant to string using Debug
485                            let debug_str = format!("{:?}", format);
486                            // Extract format name (e.g., "Email" from "Email" or "StringFormat::Email")
487                            let format_name = debug_str
488                                .split("::")
489                                .last()
490                                .unwrap_or(&debug_str)
491                                .to_lowercase();
492                            Some(format_name)
493                        }
494                        openapiv3::VariantOrUnknownOrEmpty::Unknown(s) => Some(s.clone()),
495                        _ => None,
496                    };
497
498                    if let Some(format_str) = format_str_opt {
499                        match format_str.as_str() {
500                            "email" => zod_expr = format!("{}.email()", zod_expr),
501                            "uri" | "url" => zod_expr = format!("{}.url()", zod_expr),
502                            "uuid" => zod_expr = format!("{}.uuid()", zod_expr),
503                            "date-time" | "datetime" | "date_time" => {
504                                zod_expr = format!("{}.datetime()", zod_expr)
505                            }
506                            _ => {}
507                        }
508                    }
509
510                    Ok(zod_expr)
511                }
512                Type::Number(number_type) => {
513                    let mut zod_expr = format!("{}z.number()", indent_str);
514
515                    // Add number constraints
516                    if let Some(minimum) = number_type.minimum {
517                        zod_expr = format!("{}.min({})", zod_expr, minimum);
518                    }
519                    if let Some(maximum) = number_type.maximum {
520                        zod_expr = format!("{}.max({})", zod_expr, maximum);
521                    }
522                    if let Some(multiple_of) = number_type.multiple_of {
523                        zod_expr = format!("{}.multipleOf({})", zod_expr, multiple_of);
524                    }
525
526                    Ok(zod_expr)
527                }
528                Type::Integer(integer_type) => {
529                    let mut zod_expr = format!("{}z.number().int()", indent_str);
530
531                    // Add integer constraints
532                    if let Some(minimum) = integer_type.minimum {
533                        zod_expr = format!("{}.min({})", zod_expr, minimum);
534                    }
535                    if let Some(maximum) = integer_type.maximum {
536                        zod_expr = format!("{}.max({})", zod_expr, maximum);
537                    }
538                    if let Some(multiple_of) = integer_type.multiple_of {
539                        zod_expr = format!("{}.multipleOf({})", zod_expr, multiple_of);
540                    }
541
542                    Ok(zod_expr)
543                }
544                Type::Boolean(_) => Ok(format!("{}z.boolean()", indent_str)),
545                Type::Array(array_type) => {
546                    let item_zod = if let Some(items) = &array_type.items {
547                        match items {
548                            ReferenceOr::Reference { reference } => {
549                                if let Some(ref_name) = get_schema_name_from_ref(reference) {
550                                    // Check if this $ref points to a top-level enum schema
551                                    let schema_enum_key = format!("schema:{}", ref_name);
552                                    let enum_name_opt = enum_registry.get(&schema_enum_key);
553
554                                    let schema_ref = if let Some(enum_name) = enum_name_opt {
555                                        // It's an enum, use the enum schema name
556                                        if common_schemas.contains(&ref_name) {
557                                            format!("Common.{}Schema", enum_name)
558                                        } else {
559                                            format!("{}Schema", enum_name)
560                                        }
561                                    } else {
562                                        // Not an enum, use the schema name
563                                        if common_schemas.contains(&ref_name) {
564                                            format!("Common.{}Schema", to_pascal_case(&ref_name))
565                                        } else {
566                                            format!("{}Schema", to_pascal_case(&ref_name))
567                                        }
568                                    };
569
570                                    // If already processed, use lazy reference to avoid infinite recursion
571                                    if processed.contains(&ref_name) {
572                                        format!("{}z.lazy(() => {})", indent_str, schema_ref)
573                                    } else {
574                                        let resolved =
575                                            resolve_ref(openapi, reference).map_err(|e| {
576                                                crate::error::SchemaError::InvalidReference {
577                                                    ref_path: format!("Failed to resolve: {}", e),
578                                                }
579                                            })?;
580                                        if let ReferenceOr::Item(item_schema) = resolved {
581                                            // Check if it's an object that needs to be extracted
582                                            if matches!(
583                                                &item_schema.schema_kind,
584                                                SchemaKind::Type(Type::Object(_))
585                                            ) {
586                                                generate_zod_for_schema(
587                                                    openapi,
588                                                    &ref_name,
589                                                    &item_schema,
590                                                    zod_schemas,
591                                                    processed,
592                                                    enum_registry,
593                                                    None,
594                                                    common_schemas,
595                                                    template_engine,
596                                                    spec_name,
597                                                )?;
598                                                format!(
599                                                    "{}z.lazy(() => {})",
600                                                    indent_str, schema_ref
601                                                )
602                                            } else {
603                                                schema_to_zod(
604                                                    openapi,
605                                                    &item_schema,
606                                                    zod_schemas,
607                                                    processed,
608                                                    indent,
609                                                    enum_registry,
610                                                    context,
611                                                    current_schema_name,
612                                                    common_schemas,
613                                                    template_engine,
614                                                    spec_name,
615                                                )?
616                                            }
617                                        } else {
618                                            format!("{}z.lazy(() => {})", indent_str, schema_ref)
619                                        }
620                                    }
621                                } else {
622                                    format!("{}z.any()", indent_str)
623                                }
624                            }
625                            ReferenceOr::Item(item_schema) => {
626                                // If it's an object, we need to generate it inline or as a separate schema
627                                if matches!(
628                                    &item_schema.schema_kind,
629                                    SchemaKind::Type(Type::Object(_))
630                                ) {
631                                    // Generate object fields
632                                    let object_fields = schema_to_zod(
633                                        openapi,
634                                        item_schema,
635                                        zod_schemas,
636                                        processed,
637                                        indent + 1,
638                                        enum_registry,
639                                        None,
640                                        current_schema_name,
641                                        common_schemas,
642                                        template_engine,
643                                        spec_name,
644                                    )?;
645                                    // Wrap in z.object()
646                                    format!(
647                                        "{}z.object({{\n{}\n{}}})",
648                                        indent_str, object_fields, indent_str
649                                    )
650                                } else {
651                                    schema_to_zod(
652                                        openapi,
653                                        item_schema,
654                                        zod_schemas,
655                                        processed,
656                                        indent,
657                                        enum_registry,
658                                        None,
659                                        current_schema_name,
660                                        common_schemas,
661                                        template_engine,
662                                        spec_name,
663                                    )?
664                                }
665                            }
666                        }
667                    } else {
668                        format!("{}z.any()", indent_str)
669                    };
670
671                    let mut array_zod = format!("{}z.array({})", indent_str, item_zod.trim_start());
672
673                    // Add array constraints
674                    if let Some(min_items) = array_type.min_items {
675                        array_zod = format!("{}.min({})", array_zod, min_items);
676                    }
677                    if let Some(max_items) = array_type.max_items {
678                        array_zod = format!("{}.max({})", array_zod, max_items);
679                    }
680
681                    Ok(array_zod)
682                }
683                Type::Object(object_type) => {
684                    if !object_type.properties.is_empty() {
685                        let mut fields = Vec::new();
686                        // Get parent schema name from context if available, otherwise use current_schema_name parameter
687                        // For object properties, use the current schema name as parent
688                        let parent_schema_for_props = if let Some((_, parent)) = context {
689                            if !parent.is_empty() {
690                                parent.to_string()
691                            } else if let Some(current) = current_schema_name {
692                                current.to_string()
693                            } else {
694                                String::new()
695                            }
696                        } else if let Some(current) = current_schema_name {
697                            current.to_string()
698                        } else {
699                            String::new()
700                        };
701
702                        for (prop_name, prop_schema_ref) in object_type.properties.iter() {
703                            let prop_zod = match prop_schema_ref {
704                                ReferenceOr::Reference { reference } => {
705                                    // For $ref properties, prefer enum schemas if the target is a top-level enum,
706                                    // otherwise use the referenced object schema (possibly lazily).
707                                    if let Some(ref_name) = get_schema_name_from_ref(reference) {
708                                        // If this $ref points to a top-level enum schema, use the enum schema directly
709                                        let schema_enum_key = format!("schema:{}", ref_name);
710                                        if let Some(enum_name) = enum_registry.get(&schema_enum_key)
711                                        {
712                                            // Clone enum_name to avoid borrow checker issues
713                                            let enum_name = enum_name.clone();
714
715                                            // Check if enum schema has already been generated
716                                            let schema_already_generated =
717                                                zod_schemas.iter().any(|s| {
718                                                    s.content
719                                                        .contains(&format!("{}Schema", enum_name))
720                                                });
721
722                                            // If not generated yet, generate it
723                                            if !schema_already_generated {
724                                                if !processed.contains(&ref_name) {
725                                                    if let Ok(ReferenceOr::Item(ref_schema)) =
726                                                        resolve_ref(openapi, reference)
727                                                    {
728                                                        generate_zod_for_schema(
729                                                            openapi,
730                                                            &ref_name,
731                                                            &ref_schema,
732                                                            zod_schemas,
733                                                            processed,
734                                                            enum_registry,
735                                                            Some(&parent_schema_for_props),
736                                                            common_schemas,
737                                                            template_engine,
738                                                            spec_name,
739                                                        )?;
740                                                    }
741                                                } else {
742                                                    // Schema was processed but enum schema not generated
743                                                    // This can happen if the enum was registered from TypeScript generation
744                                                    // We need to resolve the schema and generate the enum schema
745                                                    if let Ok(ReferenceOr::Item(ref_schema)) =
746                                                        resolve_ref(openapi, reference)
747                                                    {
748                                                        // Extract enum values from the schema
749                                                        if let SchemaKind::Type(Type::String(
750                                                            string_type,
751                                                        )) = &ref_schema.schema_kind
752                                                        {
753                                                            if !string_type.enumeration.is_empty() {
754                                                                let mut enum_values: Vec<String> =
755                                                                    string_type
756                                                                        .enumeration
757                                                                        .iter()
758                                                                        .filter_map(|v| {
759                                                                            v.as_ref().cloned()
760                                                                        })
761                                                                        .collect();
762                                                                if !enum_values.is_empty() {
763                                                                    enum_values.sort();
764                                                                    if let Some(engine) =
765                                                                        template_engine
766                                                                    {
767                                                                        let context =
768                                                                            ZodContext::enum_schema(
769                                                                                enum_name.clone(),
770                                                                                enum_values.clone(),
771                                                                                spec_name.map(
772                                                                                    |s| {
773                                                                                        s.to_string(
774                                                                                        )
775                                                                                    },
776                                                                                ),
777                                                                            );
778                                                                        let content = engine
779                                                                            .render(
780                                                                                TemplateId::ZodEnum,
781                                                                                &context,
782                                                                            )?;
783                                                                        zod_schemas.push(
784                                                                            ZodSchema { content },
785                                                                        );
786                                                                    } else {
787                                                                        let enum_schema =
788                                                                            generate_enum_zod(
789                                                                                &enum_name,
790                                                                                &enum_values,
791                                                                            );
792                                                                        zod_schemas
793                                                                            .push(enum_schema);
794                                                                    }
795                                                                }
796                                                            }
797                                                        }
798                                                    }
799                                                }
800                                            }
801
802                                            let schema_ref = if common_schemas.contains(&ref_name) {
803                                                format!("Common.{}Schema", enum_name)
804                                            } else {
805                                                format!("{}Schema", enum_name)
806                                            };
807                                            format!("{}{}", indent_str, schema_ref)
808                                        } else {
809                                            // Generate the referenced schema if not already processed
810                                            if !processed.contains(&ref_name) {
811                                                if let Ok(ReferenceOr::Item(ref_schema)) =
812                                                    resolve_ref(openapi, reference)
813                                                {
814                                                    // Generate both enum and object schemas
815                                                    generate_zod_for_schema(
816                                                        openapi,
817                                                        &ref_name,
818                                                        &ref_schema,
819                                                        zod_schemas,
820                                                        processed,
821                                                        enum_registry,
822                                                        Some(&parent_schema_for_props),
823                                                        common_schemas,
824                                                        template_engine,
825                                                        spec_name,
826                                                    )?;
827                                                }
828                                            }
829                                            // Check if this is an enum schema (even if not found in registry yet)
830                                            let schema_enum_key = format!("schema:{}", ref_name);
831                                            let enum_name_opt = enum_registry.get(&schema_enum_key);
832
833                                            if let Some(enum_name) = enum_name_opt {
834                                                // It's an enum, use the enum schema name directly (no lazy needed)
835                                                let schema_ref =
836                                                    if common_schemas.contains(&ref_name) {
837                                                        format!("Common.{}Schema", enum_name)
838                                                    } else {
839                                                        format!("{}Schema", enum_name)
840                                                    };
841                                                format!("{}{}", indent_str, schema_ref)
842                                            } else {
843                                                // Not an enum, use the schema name with lazy
844                                                let schema_ref = if common_schemas
845                                                    .contains(&ref_name)
846                                                {
847                                                    format!(
848                                                        "Common.{}Schema",
849                                                        to_pascal_case(&ref_name)
850                                                    )
851                                                } else {
852                                                    format!("{}Schema", to_pascal_case(&ref_name))
853                                                };
854                                                format!(
855                                                    "{}z.lazy(() => {})",
856                                                    indent_str, schema_ref
857                                                )
858                                            }
859                                        }
860                                    } else {
861                                        format!("{}z.any()", indent_str)
862                                    }
863                                }
864                                ReferenceOr::Item(prop_schema) => schema_to_zod(
865                                    openapi,
866                                    prop_schema,
867                                    zod_schemas,
868                                    processed,
869                                    indent + 1,
870                                    enum_registry,
871                                    Some((prop_name, &parent_schema_for_props)),
872                                    current_schema_name,
873                                    common_schemas,
874                                    template_engine,
875                                    spec_name,
876                                )?,
877                            };
878
879                            let required = object_type.required.contains(prop_name);
880
881                            let nullable = prop_schema_ref
882                                .as_item()
883                                .map(|s| s.schema_data.nullable)
884                                .unwrap_or(false);
885
886                            let mut zod_expr = prop_zod.trim_start().to_string();
887                            if nullable {
888                                zod_expr = format!("{}.nullable()", zod_expr);
889                            }
890                            if !required {
891                                zod_expr = format!("{}.optional()", zod_expr);
892                            }
893
894                            fields.push(format!(
895                                "{}{}: {},",
896                                "  ".repeat(indent + 1),
897                                sanitize_property_name(prop_name),
898                                zod_expr
899                            ));
900                        }
901                        Ok(fields.join("\n"))
902                    } else {
903                        Ok(format!("{}z.record(z.string(), z.any())", indent_str))
904                    }
905                }
906            }
907        }
908        SchemaKind::Any(_) => Ok(format!("{}z.any()", indent_str)),
909        SchemaKind::OneOf { one_of, .. } => {
910            let mut variant_schemas = Vec::new();
911            for item in one_of {
912                match item {
913                    ReferenceOr::Reference { reference } => {
914                        if let Some(ref_name) = get_schema_name_from_ref(reference) {
915                            // Check if this $ref points to a top-level enum schema
916                            let schema_enum_key = format!("schema:{}", ref_name);
917                            let enum_name_opt = enum_registry.get(&schema_enum_key);
918
919                            let schema_ref = if let Some(enum_name) = enum_name_opt {
920                                // It's an enum, use the enum schema name
921                                if common_schemas.contains(&ref_name) {
922                                    format!("Common.{}Schema", enum_name)
923                                } else {
924                                    format!("{}Schema", enum_name)
925                                }
926                            } else {
927                                // Not an enum, use the schema name
928                                if common_schemas.contains(&ref_name) {
929                                    format!("Common.{}Schema", to_pascal_case(&ref_name))
930                                } else {
931                                    format!("{}Schema", to_pascal_case(&ref_name))
932                                }
933                            };
934                            variant_schemas
935                                .push(format!("{}z.lazy(() => {})", indent_str, schema_ref));
936                        } else {
937                            variant_schemas.push(format!("{}z.any()", indent_str));
938                        }
939                    }
940                    ReferenceOr::Item(item_schema) => {
941                        let item_zod = schema_to_zod(
942                            openapi,
943                            item_schema,
944                            zod_schemas,
945                            processed,
946                            indent,
947                            enum_registry,
948                            None,
949                            current_schema_name,
950                            common_schemas,
951                            template_engine,
952                            spec_name,
953                        )?;
954                        variant_schemas.push(item_zod);
955                    }
956                }
957            }
958            if variant_schemas.is_empty() {
959                Ok(format!("{}z.any()", indent_str))
960            } else {
961                Ok(format!(
962                    "{}z.union([{}])",
963                    indent_str,
964                    variant_schemas.join(", ")
965                ))
966            }
967        }
968        SchemaKind::AllOf { all_of, .. } => {
969            let mut all_schemas = Vec::new();
970            for item in all_of {
971                match item {
972                    ReferenceOr::Reference { reference } => {
973                        if let Some(ref_name) = get_schema_name_from_ref(reference) {
974                            let schema_ref = if common_schemas.contains(&ref_name) {
975                                format!("Common.{}Schema", to_pascal_case(&ref_name))
976                            } else {
977                                format!("{}Schema", to_pascal_case(&ref_name))
978                            };
979                            all_schemas.push(format!("{}z.lazy(() => {})", indent_str, schema_ref));
980                        } else {
981                            all_schemas.push(format!("{}z.any()", indent_str));
982                        }
983                    }
984                    ReferenceOr::Item(item_schema) => {
985                        let item_zod = schema_to_zod(
986                            openapi,
987                            item_schema,
988                            zod_schemas,
989                            processed,
990                            indent,
991                            enum_registry,
992                            None,
993                            current_schema_name,
994                            common_schemas,
995                            template_engine,
996                            spec_name,
997                        )?;
998                        all_schemas.push(item_zod);
999                    }
1000                }
1001            }
1002            if all_schemas.is_empty() {
1003                Ok(format!("{}z.any()", indent_str))
1004            } else if all_schemas.len() == 1 {
1005                Ok(all_schemas[0].clone())
1006            } else {
1007                // AllOf represents intersection: all schemas must be satisfied
1008                // Zod uses .and() for intersection. Chain them: schema1.and(schema2).and(schema3)
1009                let mut result = all_schemas[0].clone();
1010                for schema in all_schemas.iter().skip(1) {
1011                    result = format!("{}.and({})", result.trim(), schema.trim());
1012                }
1013                Ok(result)
1014            }
1015        }
1016        SchemaKind::AnyOf { any_of, .. } => {
1017            // AnyOf is treated same as OneOf (union)
1018            let mut variant_schemas = Vec::new();
1019            for item in any_of {
1020                match item {
1021                    ReferenceOr::Reference { reference } => {
1022                        if let Some(ref_name) = get_schema_name_from_ref(reference) {
1023                            // Check if this $ref points to a top-level enum schema
1024                            let schema_enum_key = format!("schema:{}", ref_name);
1025                            let enum_name_opt = enum_registry.get(&schema_enum_key);
1026
1027                            let schema_ref = if let Some(enum_name) = enum_name_opt {
1028                                // It's an enum, use the enum schema name
1029                                if common_schemas.contains(&ref_name) {
1030                                    format!("Common.{}Schema", enum_name)
1031                                } else {
1032                                    format!("{}Schema", enum_name)
1033                                }
1034                            } else {
1035                                // Not an enum, use the schema name
1036                                if common_schemas.contains(&ref_name) {
1037                                    format!("Common.{}Schema", to_pascal_case(&ref_name))
1038                                } else {
1039                                    format!("{}Schema", to_pascal_case(&ref_name))
1040                                }
1041                            };
1042                            variant_schemas
1043                                .push(format!("{}z.lazy(() => {})", indent_str, schema_ref));
1044                        } else {
1045                            variant_schemas.push(format!("{}z.any()", indent_str));
1046                        }
1047                    }
1048                    ReferenceOr::Item(item_schema) => {
1049                        let item_zod = schema_to_zod(
1050                            openapi,
1051                            item_schema,
1052                            zod_schemas,
1053                            processed,
1054                            indent,
1055                            enum_registry,
1056                            None,
1057                            current_schema_name,
1058                            common_schemas,
1059                            template_engine,
1060                            spec_name,
1061                        )?;
1062                        variant_schemas.push(item_zod);
1063                    }
1064                }
1065            }
1066            if variant_schemas.is_empty() {
1067                Ok(format!("{}z.any()", indent_str))
1068            } else {
1069                Ok(format!(
1070                    "{}z.union([{}])",
1071                    indent_str,
1072                    variant_schemas.join(", ")
1073                ))
1074            }
1075        }
1076        SchemaKind::Not { .. } => Ok(format!("{}z.any()", indent_str)),
1077    }
1078}
1079
1080fn generate_enum_zod(name: &str, values: &[String]) -> ZodSchema {
1081    let enum_name = to_pascal_case(name);
1082    let enum_values = values
1083        .iter()
1084        .map(|v| format!("\"{}\"", v))
1085        .collect::<Vec<_>>()
1086        .join(", ");
1087
1088    ZodSchema {
1089        content: format!(
1090            "export const {}Schema = z.enum([{}]);",
1091            enum_name, enum_values
1092        ),
1093    }
1094}