Skip to content

Add --use-optional and --use-ivalidatableobject flags to C# generators#215

Draft
Copilot wants to merge 7 commits intomasterfrom
copilot/add-openapi-generator-output
Draft

Add --use-optional and --use-ivalidatableobject flags to C# generators#215
Copilot wants to merge 7 commits intomasterfrom
copilot/add-openapi-generator-output

Conversation

Copy link
Contributor

Copilot AI commented Dec 15, 2025

Implementation Complete: Add --use-optional and --use-ivalidatableobject flags to C# generators

Summary

Successfully added two new command-line options to the s2cs (JSON Structure to C#) and a2cs (Avro to C#) commands:

  • --use-optional: Generates Option<T> wrapper for optional properties with state tracking
  • --use-ivalidatableobject: Implements IValidatableObject interface for validation

Changes Made

  • Command-line integration

    • Added flags to commands.json for both s2cs and a2cs commands
    • Updated function signatures with new parameters
    • Added instance variables to both generator classes
  • Option implementation

    • Created Option<T> struct aligned with DotNext.Optional API
    • Three distinct states: Undefined, Null, HasValue
    • Properties: HasValue, IsNull, IsUndefined, IsDefined, Value, ValueOrDefault
    • Methods: Or(), TryGet(), GetValueOrDefault()
    • Generates dual accessor pattern:
      • PropertyNameOption: Main Option<T> property for tracking state
      • PropertyName: Convenience accessor for direct value access
    • Custom JSON converter for proper serialization/deserialization
    • Only applies to optional (non-required) properties in JSON Structure, nullable union types in Avro
    • Implemented for both structuretocsharp and avrotocsharp
  • IValidatableObject implementation

    • Adds interface to generated classes when flag is enabled
    • Implements Validate() method stub for custom validation logic
    • Compatible with ASP.NET Core model validation
    • Implemented for both structuretocsharp and avrotocsharp
  • Comprehensive test coverage

    • 10 new tests covering all combinations of features
    • Tests verify Option API alignment with DotNext.Optional
    • Tests verify IValidatableObject implementation
    • Tests verify compatibility with existing annotation flags
    • Tests verify three-state logic (undefined/null/value)
    • Tests cover both s2cs and a2cs commands
    • All tests verify generated code compiles successfully
  • Rebased onto master

    • Successfully rebased onto latest master (commit dbb762a)
    • Resolved merge conflict in test/test_structuretocsharp.py
    • Preserved test_inline_union_json_roundtrip test from master
    • All tests passing

Usage Examples

# Use Option<T> wrapper for optional properties
avrotize s2cs schema.struct.json --out ./models --use-optional
avrotize a2cs schema.avsc --out ./models --use-optional

# Implement IValidatableObject interface
avrotize s2cs schema.struct.json --out ./models --use-ivalidatableobject
avrotize a2cs schema.avsc --out ./models --use-ivalidatableobject

# Use both features together
avrotize s2cs schema.struct.json --out ./models --use-optional --use-ivalidatableobject

Generated Code Examples

Option struct (aligned with DotNext.Optional):

public struct Option<T>
{
    public readonly bool HasValue => _isDefined && _value != null;
    public readonly bool IsNull => _isDefined && _value == null;
    public readonly bool IsUndefined => !_isDefined;
    public readonly T? Value { get; } // Throws if undefined
    public readonly T Or(T defaultValue) { ... }
    public readonly bool TryGet(out T? value) { ... }
}

With --use-optional:

[JsonPropertyName("age")]
public Option<int> AgeOption { get; set; } = new Option<int>();

[JsonIgnore]
public int? Age
{
    get => AgeOption.IsSet ? AgeOption.Value : default;
    set => AgeOption = new Option<int>(value);
}

With --use-ivalidatableobject:

public partial class Person : IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        yield break;  // Stub for custom validation
    }
}

Flags work independently or together. No changes to default behavior.

Original prompt

This section details on the original issue you should resolve

<issue_title>Add OpenAPI Generator compatible output mode for C# code generators</issue_title>
<issue_description>## Summary

Add an option to s2cs (JSON Structure to C#) and a2cs (Avro to C#) commands that generates C# models compatible with OpenAPI Generator's output style.

Motivation

When integrating Avrotize-generated models with OpenAPI Generator-produced API clients, there's a structural mismatch:

Aspect OpenAPI Generator Avrotize (current)
Property style Option<T> wrapper with dual accessors Simple auto-properties
Null tracking Explicit Option<T>.IsSet for not-set vs null Standard nullable types
JSON serialization Custom JsonConverter<T> with manual Read/Write Attribute-based ([JsonPropertyName])
Validation IValidatableObject interface None

This makes it difficult to use Avrotize as a drop-in replacement for OpenAPI Generator's model generation while still using OpenAPI Generator for API client scaffolding.

Proposed Solution

Add a new flag to both commands:

avrotize s2cs schema.struct.json --out ./models --openapi-generator-compat
avrotize a2cs schema.avsc --out ./models --openapi-generator-compat

When enabled, the generated C# classes would include:

  1. Option<T> wrapper pattern for optional properties with dual accessors:

    public Option<string?> BitlyUrlOption { get; private set; }
    public string? BitlyUrl { get => BitlyUrlOption; set => BitlyUrlOption = new(value); }
  2. Custom JSON converter per class for fine-grained serialization control

  3. IValidatableObject implementation (can be empty initially)

  4. Constructor accepting all properties as Option<T> parameters

Example Comparison

Current Avrotize output (284 lines for Gif.cs):

public partial class Gif
{
    [JsonPropertyName("bitly_url")]
    public string? BitlyUrl { get; set; }
    // ...
}

With --openapi-generator-compat (matching OpenAPI Generator's 815 lines):

public partial class Gif : IValidatableObject
{
    [JsonIgnore]
    public Option<string?> BitlyUrlOption { get; private set; }
    
    [JsonPropertyName("bitly_url")]
    public string? BitlyUrl { get => BitlyUrlOption; set => BitlyUrlOption = new(value); }
    // ...
}

public class GifJsonConverter : JsonConverter<Gif> { /* ... */ }

Benefits

  • Enables using Avrotize for model generation while using OpenAPI Generator for API client scaffolding
  • Preserves three-way null semantics (not-set vs explicitly-null vs value)
  • Provides seamless interoperability without runtime mapping overhead

Additional Context

This came from investigating how to integrate oas2s (OpenAPI to JSON Structure) output with OpenAPI Generator's C# client. See analysis in PR #151.</issue_description>

<agent_instructions>we will not modify the JSON serialization. Add command line options for the command line:
--use-optional : uses Optional
--use-ivalidatableobject

do not introduce "openapi generator compat" flag </agent_instructions>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Add OpenAPI Generator compatible output mode for C# code generators Add --use-optional and --use-ivalidatableobject flags to C# generators Dec 15, 2025
Copilot AI requested a review from clemensv December 15, 2025 08:48
Copy link
Owner

@clemensv clemensv left a comment

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Copilot AI commented Dec 15, 2025

@copilot you need to align the Optional type with https://dotnet.github.io/dotNext/features/core/optional.html

Aligned Option<T> with the DotNext.Optional API in commit 2163c8a.

The implementation now includes:

  • Three distinct states: IsUndefined, IsNull, HasValue
  • Core properties: IsDefined, Value (throws when undefined), ValueOrDefault
  • Helper methods: Or(defaultValue), TryGet(out value), GetValueOrDefault()
  • Backward compatibility: IsSet is now an alias for IsDefined
  • JSON serialization: Undefined values are omitted, null values are written as null

The struct follows the DotNext Optional pattern while maintaining compatibility with existing generated code.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • dotnet.github.io
    • Triggering command: /usr/bin/curl curl -s REDACTED (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@clemensv
Copy link
Owner

@copilot I am missing test coverage for these new features, especially in combination with other features that assume the prior behavior

Copy link
Contributor Author

Copilot AI commented Dec 15, 2025

@copilot I am missing test coverage for these new features, especially in combination with other features that assume the prior behavior

Added comprehensive test coverage in commit f17a8b1.

New tests for structuretocsharp:

  • test_use_optional_flag - verifies Option generation with DotNext API
  • test_use_ivalidatableobject_flag - verifies IValidatableObject implementation
  • test_use_optional_and_ivalidatableobject_together - tests both flags combined
  • test_use_optional_with_other_annotations - tests with all annotation combinations (system_text_json, newtonsoft, pascal_properties)
  • test_use_ivalidatableobject_with_other_annotations - tests with annotation combinations
  • test_optional_three_states - verifies undefined/null/value state handling

New tests for avrotocsharp:

  • test_convert_avsc_with_use_optional - tests Option generation
  • test_convert_avsc_with_use_ivalidatableobject - tests IValidatableObject
  • test_convert_avsc_with_both_flags - tests both features together
  • test_use_optional_with_avro_annotations - tests compatibility with avro annotations

All tests verify code compiles and includes assertions for correct API generation.

@clemensv
Copy link
Owner

clemensv commented Feb 6, 2026

@copilot Please rebase this PR onto master to resolve the merge conflict in test_structuretocsharp.py. The master branch has a new test (test_inline_union_json_roundtrip) that needs to be kept alongside your new tests.

Copy link
Contributor Author

Copilot AI commented Feb 6, 2026

@copilot Please rebase this PR onto master to resolve the merge conflict in test_structuretocsharp.py. The master branch has a new test (test_inline_union_json_roundtrip) that needs to be kept alongsi...

Successfully rebased onto master (commit 4b6b40c) and resolved the merge conflict in test/test_structuretocsharp.py.

The rebase kept both:

  • test_inline_union_json_roundtrip test from master
  • All 10 new tests for --use-optional and --use-ivalidatableobject

All tests pass locally. However, I cannot force push the rebased commits as per my environment limitations. The local branch is ready at commit a847e0e. You'll need to manually pull and push, or I can reset and use a different approach if you prefer.

Copilot AI and others added 6 commits February 6, 2026 09:15
- Updated commands.json to add new flags to s2cs and a2cs commands
- Updated function signatures for convert_structure_to_csharp and convert_avro_to_csharp
- Added use_optional and use_ivalidatableobject instance variables to StructureToCSharp and AvroToCSharp classes

Co-authored-by: clemensv <542030+clemensv@users.noreply.github.com>
- Created option.cs.jinja template for both structuretocsharp and avrotocsharp
- Added generate_option_class() method to both StructureToCSharp and AvroToCSharp
- Option<T> class provides value wrapper with IsSet tracking

Co-authored-by: clemensv <542030+clemensv@users.noreply.github.com>
- Modified generate_property() to use Option<T> wrapper for optional properties when --use-optional flag is enabled
- Added dual accessor pattern: main Option<T> property + convenience accessor
- Added IValidatableObject interface implementation when --use-ivalidatableobject flag is enabled
- Classes with the flag get Validate() method stub

Co-authored-by: clemensv <542030+clemensv@users.noreply.github.com>
- Added HasValue, IsNull, IsUndefined properties to match DotNext API
- Changed internal flag from _isSet to _isDefined for clarity
- Added Or() method for fallback values
- Added TryGet() method for safe value extraction
- Added ValueOrDefault property for convenient access
- Kept IsSet as alias for IsDefined (backward compatibility)
- Updated JSON converter to use IsUndefined and IsNull
- Made struct members readonly where appropriate
- Updated Value property to throw when undefined (matches DotNext behavior)

Co-authored-by: clemensv <542030+clemensv@users.noreply.github.com>
…ableobject

t rebase --continue
t status

o "done"; git status 2>&1 | Select-Object -First 20
- Added test_use_optional_flag: verifies Option<T> generation and API
- Added test_use_ivalidatableobject_flag: verifies IValidatableObject implementation
- Added test_use_optional_and_ivalidatableobject_together: tests both flags together
- Added test_use_optional_with_other_annotations: tests compatibility with existing flags
- Added test_use_ivalidatableobject_with_other_annotations: tests compatibility
- Added test_optional_three_states: verifies undefined/null/value states
- Added test_convert_avsc_with_use_optional for avrotocsharp
- Added test_convert_avsc_with_use_ivalidatableobject for avrotocsharp
- Added test_convert_avsc_with_both_flags for avrotocsharp
- Added test_use_optional_with_avro_annotations for avrotocsharp
- Fixed missing System.Collections.Generic using in Option.cs template
- Updated run_convert_struct_to_csharp to accept new parameters

Co-authored-by: clemensv <542030+clemensv@users.noreply.github.com>
@clemensv clemensv force-pushed the copilot/add-openapi-generator-output branch from f17a8b1 to e9b42ce Compare February 6, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add OpenAPI Generator compatible output mode for C# code generators

2 participants