|
| 1 | +# Design Document: Dictionary Schema Support |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This design document describes the implementation of proper dictionary type handling in the Oproto.Lambda.OpenApi source generator. The solution adds dictionary detection logic to the type analysis pipeline and generates correct OpenAPI `additionalProperties` schemas for dictionary types. |
| 6 | + |
| 7 | +### OpenAPI Dictionary Representation |
| 8 | + |
| 9 | +Per the [OpenAPI Specification](https://swagger.io/docs/specification/data-models/dictionaries/), dictionaries (maps, hashmaps, associative arrays) are represented using `type: object` with `additionalProperties` defining the value type. This is the standard pattern recognized by all major code generators including: |
| 10 | + |
| 11 | +- **Kiota** (Microsoft) - generates `IDictionary<string, T>` in C# |
| 12 | +- **OpenAPI Generator** - generates `Dictionary<string, T>` in C# |
| 13 | +- **NSwag** - generates `IDictionary<string, T>` in C# |
| 14 | + |
| 15 | +OpenAPI only supports string keys for dictionaries, which aligns with JSON's object key constraints. |
| 16 | + |
| 17 | +## Architecture |
| 18 | + |
| 19 | +The implementation follows the existing partial class pattern used by the source generator, adding dictionary-specific logic to the type detection and schema creation pipeline. |
| 20 | + |
| 21 | +``` |
| 22 | +┌─────────────────────────────────────────────────────────────────┐ |
| 23 | +│ CreateSchema (Entry Point) │ |
| 24 | +└─────────────────────────────────────────────────────────────────┘ |
| 25 | + │ |
| 26 | + ▼ |
| 27 | +┌─────────────────────────────────────────────────────────────────┐ |
| 28 | +│ 1. TryCreateNullableSchema (existing) │ |
| 29 | +│ 2. TryCreateSpecialTypeSchema (existing - Ulid) │ |
| 30 | +│ 3. TryCreateCollectionSchema (existing - arrays/lists) │ |
| 31 | +│ 4. TryCreateDictionarySchema (NEW) ◄─────────────────────────│ |
| 32 | +│ 5. IsSimpleType → CreateSimpleTypeSchema (existing) │ |
| 33 | +│ 6. CreateComplexTypeSchema (existing - fallback) │ |
| 34 | +└─────────────────────────────────────────────────────────────────┘ |
| 35 | +``` |
| 36 | + |
| 37 | +The key insight is that dictionary detection must occur: |
| 38 | +- After nullable handling (to unwrap `Nullable<T>`) |
| 39 | +- After collection handling (dictionaries are not arrays) |
| 40 | +- Before complex type handling (to prevent incorrect object schema generation) |
| 41 | + |
| 42 | +## Components and Interfaces |
| 43 | + |
| 44 | +### New Methods in OpenApiSpecGenerator_Types.cs |
| 45 | + |
| 46 | +```csharp |
| 47 | +/// <summary> |
| 48 | +/// Determines if a type is a dictionary type (Dictionary, IDictionary, IReadOnlyDictionary). |
| 49 | +/// </summary> |
| 50 | +/// <param name="typeSymbol">The type to check</param> |
| 51 | +/// <param name="keyType">Output parameter for the dictionary's key type</param> |
| 52 | +/// <param name="valueType">Output parameter for the dictionary's value type</param> |
| 53 | +/// <returns>True if the type is a dictionary type</returns> |
| 54 | +private bool IsDictionaryType(ITypeSymbol typeSymbol, out ITypeSymbol keyType, out ITypeSymbol valueType) |
| 55 | +``` |
| 56 | + |
| 57 | +### New Methods in OpenApiSpecGenerator_Schema.cs |
| 58 | + |
| 59 | +```csharp |
| 60 | +/// <summary> |
| 61 | +/// Attempts to create a schema for dictionary types. |
| 62 | +/// </summary> |
| 63 | +/// <param name="typeSymbol">The type symbol to check for dictionary</param> |
| 64 | +/// <param name="memberSymbol">The member symbol for additional metadata</param> |
| 65 | +/// <param name="schema">The output schema if the type is a dictionary</param> |
| 66 | +/// <returns>True if a dictionary schema was created, false otherwise</returns> |
| 67 | +private bool TryCreateDictionarySchema(ITypeSymbol typeSymbol, ISymbol memberSymbol, out OpenApiSchema schema) |
| 68 | +``` |
| 69 | + |
| 70 | +### Integration Point in CreateSchema |
| 71 | + |
| 72 | +The `CreateSchema` method in `OpenApiSpecGenerator_Schema.cs` will be modified to call `TryCreateDictionarySchema` after collection handling but before complex type handling. |
| 73 | + |
| 74 | +## Data Models |
| 75 | + |
| 76 | +### Dictionary Type Detection Patterns |
| 77 | + |
| 78 | +The following type patterns will be recognized as dictionaries: |
| 79 | + |
| 80 | +| Type Pattern | MetadataName | Detection Method | |
| 81 | +|--------------|--------------|------------------| |
| 82 | +| `Dictionary<K,V>` | `Dictionary`2` | Direct type check | |
| 83 | +| `IDictionary<K,V>` | `IDictionary`2` | Direct type check | |
| 84 | +| `IReadOnlyDictionary<K,V>` | `IReadOnlyDictionary`2` | Direct type check | |
| 85 | +| Custom types implementing IDictionary | N/A | Interface check | |
| 86 | + |
| 87 | +### Generated Schema Patterns |
| 88 | + |
| 89 | +| Input Type | Generated Schema | |
| 90 | +|------------|------------------| |
| 91 | +| `Dictionary<string, string>` | `{ "type": "object", "additionalProperties": { "type": "string" } }` | |
| 92 | +| `Dictionary<string, int>` | `{ "type": "object", "additionalProperties": { "type": "integer" } }` | |
| 93 | +| `Dictionary<string, ComplexType>` | `{ "type": "object", "additionalProperties": { "$ref": "#/components/schemas/ComplexType" } }` | |
| 94 | +| `Dictionary<string, List<T>>` | `{ "type": "object", "additionalProperties": { "type": "array", "items": {...} } }` | |
| 95 | +| `Dictionary<string, Dictionary<string, T>>` | `{ "type": "object", "additionalProperties": { "type": "object", "additionalProperties": {...} } }` | |
| 96 | + |
| 97 | +## Correctness Properties |
| 98 | + |
| 99 | +*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* |
| 100 | + |
| 101 | +### Property 1: Dictionary Type Detection |
| 102 | + |
| 103 | +*For any* type symbol that is `Dictionary<K,V>`, `IDictionary<K,V>`, `IReadOnlyDictionary<K,V>`, or implements `IDictionary<K,V>`, the `IsDictionaryType` method SHALL return true and correctly extract the key and value types. |
| 104 | + |
| 105 | +**Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5** |
| 106 | + |
| 107 | +### Property 2: Dictionary Schema Structure |
| 108 | + |
| 109 | +*For any* dictionary type, the generated OpenAPI schema SHALL have `type: "object"` and a non-null `additionalProperties` schema (not empty `properties`). |
| 110 | + |
| 111 | +**Validates: Requirements 5.2** |
| 112 | + |
| 113 | +### Property 3: Simple Value Type Schema |
| 114 | + |
| 115 | +*For any* dictionary with a simple value type (string, int, bool, decimal, DateTime, etc.), the `additionalProperties` schema SHALL have the correct OpenAPI type and format matching the value type. |
| 116 | + |
| 117 | +**Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5** |
| 118 | + |
| 119 | +### Property 4: Complex Value Type Reference |
| 120 | + |
| 121 | +*For any* dictionary with a complex (non-simple) value type, the `additionalProperties` schema SHALL contain a reference (`$ref`) to the value type's schema in components. |
| 122 | + |
| 123 | +**Validates: Requirements 3.1** |
| 124 | + |
| 125 | +### Property 5: Nullable Dictionary Handling |
| 126 | + |
| 127 | +*For any* nullable dictionary type (either `Nullable<Dictionary<K,V>>` or dictionary property with nullable annotation), the generated schema SHALL have `nullable: true`. |
| 128 | + |
| 129 | +**Validates: Requirements 4.1, 4.2** |
| 130 | + |
| 131 | +## Error Handling |
| 132 | + |
| 133 | +### Invalid Dictionary Types |
| 134 | + |
| 135 | +- If a dictionary type has non-string keys, the generator will still produce an `additionalProperties` schema (OpenAPI only supports string keys for objects) |
| 136 | +- If the value type cannot be resolved, the generator falls back to `additionalProperties: { type: "object" }` |
| 137 | + |
| 138 | +### Circular References |
| 139 | + |
| 140 | +- Dictionary value types that reference the containing type are handled by the existing circular reference detection in `_processedTypes` |
| 141 | +- Self-referential dictionaries produce schemas with `$ref` to prevent infinite recursion |
| 142 | + |
| 143 | +### Attribute Processing Errors |
| 144 | + |
| 145 | +- Invalid JSON in `[OpenApiSchema(Example = "...")]` is handled gracefully with fallback to string representation |
| 146 | +- Missing or malformed attributes are ignored, using default schema generation |
| 147 | + |
| 148 | +## Testing Strategy |
| 149 | + |
| 150 | +### Unit Tests |
| 151 | + |
| 152 | +Unit tests will verify specific examples and edge cases: |
| 153 | + |
| 154 | +1. `Dictionary<string, string>` produces correct schema |
| 155 | +2. `Dictionary<string, int>` produces correct schema with integer type |
| 156 | +3. `Dictionary<string, ComplexType>` produces schema with $ref |
| 157 | +4. `Dictionary<string, List<string>>` produces nested array schema |
| 158 | +5. `Dictionary<string, Dictionary<string, int>>` produces nested dictionary schema |
| 159 | +6. Nullable dictionary produces schema with `nullable: true` |
| 160 | +7. Dictionary with `[OpenApiSchema]` attributes applies description and example |
| 161 | +8. Custom type implementing `IDictionary<K,V>` is detected as dictionary |
| 162 | + |
| 163 | +### Property-Based Tests |
| 164 | + |
| 165 | +Property-based tests will verify universal properties using the fast-check library pattern: |
| 166 | + |
| 167 | +1. **Dictionary Detection Property**: For all generated dictionary type symbols, `IsDictionaryType` returns true |
| 168 | +2. **Schema Structure Property**: For all dictionary types, generated schema has `additionalProperties` (not empty `properties`) |
| 169 | +3. **Value Type Mapping Property**: For all dictionaries with simple value types, `additionalProperties.Type` matches the expected OpenAPI type |
| 170 | +4. **Complex Type Reference Property**: For all dictionaries with complex value types, `additionalProperties` contains a `$ref` |
| 171 | +5. **Nullable Property**: For all nullable dictionaries, schema has `nullable: true` |
| 172 | + |
| 173 | +### Test Configuration |
| 174 | + |
| 175 | +- Property tests will run minimum 100 iterations |
| 176 | +- Tests will use the existing `GenerateSchemaFromSource` helper pattern from `OpenApiGeneratorTests.cs` |
| 177 | +- Each property test will be tagged with: **Feature: dictionary-schema-support, Property N: {property_text}** |
| 178 | + |
0 commit comments