Skip to content

Conversation

Fellmonkey
Copy link
Contributor

@Fellmonkey Fellmonkey commented Aug 2, 2025

What does this PR do?

This PR refactors the JSON serialization and deserialization logic in the .NET SDK template to improve robustness and fix critical issues:
Key Changes:

  1. Fixed infinite recursion bug in ObjectToInferredTypesConverter: Replaced the problematic approach of using JsonSerializer.Deserialize recursively within the converter itself, which caused StackOverflowException for nested objects and arrays.

  2. Improved JSON type inference: Switched from JsonTokenType to JsonElement.ValueKind for more accurate and reliable type detection, providing better handling of all JSON value types including null and undefined values.

  3. Eliminates the risk of leaking JsonElement instances into the resulting object graph, simplifying model deserialization and removing the need for special handling of JsonElement in generated code.

  4. Streamlined model deserialization: Simplified the generated model deserialization logic by removing special handling for JsonElement objects and standardizing type conversions, making the generated code more readable and maintainable.

  5. Enhanced error handling: Added proper error handling with descriptive exceptions for unsupported JSON value kinds.

Test Plan

Testing the ObjectToInferredTypesConverter fix:

  • Create a test with nested JSON objects and arrays to verify no StackOverflowException occurs
  • Test deserialization of various JSON types (strings, numbers, booleans, null, objects, arrays)
  • Test edge cases with deeply nested structures

Testing model deserialization changes:

  • Generate SDK models with the updated template
  • Test deserialization of models with:
    • Array properties containing primitives and objects
    • Optional and required properties
    • Nested model objects
    • Various data types (string, integer, number, boolean)

Related PRs and Issues

This PR addresses potential runtime crashes and improves the overall reliability of JSON handling in generated .NET SDKs. The changes are particularly important for applications that work with complex nested JSON structures from API responses.

Related to issues with incorrect type mapping, JsonElement leakage, and runtime errors during deserialization of complex/nested JSON structures.

Have you read the Contributing Guidelines on issues?

YES

Summary by CodeRabbit

  • New Features

    • Added an exception constructor that supports including an inner exception.
  • Bug Fixes

    • More robust JSON value inference with proper handling of null/undefined, numbers, booleans, strings, dates, arrays, and objects.
    • Safer model deserialization: guards missing optional fields and reliably converts primitive and array values to prevent crashes.
  • Refactor

    • Adopted templated namespaces across generated code (including extensions and roles) for consistent package naming.

Replaces switch on JsonTokenType with a recursive method using JsonElement.ValueKind for more robust and accurate type inference. This improves handling of nested objects and arrays, and unifies the logic for converting JSON values to .NET types.
Simplifies and standardizes the deserialization of model properties from dictionaries, removing special handling for JsonElement and streamlining array and primitive type conversions. This improves code readability and maintainability in generated model classes.
@Fellmonkey Fellmonkey changed the title fix: (.NET) Improve json serialization fix: (.NET) Improve json De/serialization Aug 2, 2025
@Fellmonkey Fellmonkey mentioned this pull request Aug 2, 2025
Updated the From method in the model template to check for the existence of optional properties in the input map before assigning values. This prevents errors when optional properties are missing from the input dictionary. (for examle in model: User, :-/ )
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors JSON serialization and deserialization in the .NET SDK template to fix critical bugs and improve robustness, particularly addressing infinite recursion issues and type inference problems.

  • Fixed infinite recursion bug in ObjectToInferredTypesConverter that caused StackOverflowException
  • Improved JSON type inference by switching from JsonTokenType to JsonElement.ValueKind
  • Streamlined model deserialization by removing special JsonElement handling and standardizing type conversions

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
templates/dotnet/Package/Role.cs.twig Updated namespace to use dynamic spec title
templates/dotnet/Package/Models/Model.cs.twig Simplified model deserialization logic, removed JsonElement handling
templates/dotnet/Package/Models/InputFile.cs.twig Updated namespace import to use dynamic spec title
templates/dotnet/Package/Exception.cs.twig Added blank line formatting
templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig Complete rewrite to fix recursion bug and improve type inference

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

{%- endif %}
{%- else %}
{%- if property.type == 'array' -%}
map["{{ property.name }}"] is JsonElement jsonArrayProp{{ loop.index }} ? jsonArrayProp{{ loop.index }}.Deserialize<{{ property | typeName }}>()! : ({{ property | typeName }})map["{{ property.name }}"]
((IEnumerable<object>)map["{{ property.name }}"]).Select(x => {% if property.items.type == "string" %}x?.ToString(){% elseif property.items.type == "integer" %}{% if not property.required %}x == null ? (long?)null : {% endif %}Convert.ToInt64(x){% elseif property.items.type == "number" %}{% if not property.required %}x == null ? (double?)null : {% endif %}Convert.ToDouble(x){% elseif property.items.type == "boolean" %}{% if not property.required %}x == null ? (bool?)null : {% endif %}(bool)x{% else %}x{% endif %}).{% if property.items.type == "string" and property.required %}Where(x => x != null).{% endif %}ToList()!
Copy link
Preview

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

The null-forgiving operator ! at the end of ToList()! could hide potential null reference exceptions. If the enumerable or its elements could be null, this could cause runtime errors.

Suggested change
((IEnumerable<object>)map["{{ property.name }}"]).Select(x => {% if property.items.type == "string" %}x?.ToString(){% elseif property.items.type == "integer" %}{% if not property.required %}x == null ? (long?)null : {% endif %}Convert.ToInt64(x){% elseif property.items.type == "number" %}{% if not property.required %}x == null ? (double?)null : {% endif %}Convert.ToDouble(x){% elseif property.items.type == "boolean" %}{% if not property.required %}x == null ? (bool?)null : {% endif %}(bool)x{% else %}x{% endif %}).{% if property.items.type == "string" and property.required %}Where(x => x != null).{% endif %}ToList()!
((map["{{ property.name }}"] as IEnumerable<object>)?.Select(x => {% if property.items.type == "string" %}x?.ToString(){% elseif property.items.type == "integer" %}{% if not property.required %}x == null ? (long?)null : {% endif %}Convert.ToInt64(x){% elseif property.items.type == "number" %}{% if not property.required %}x == null ? (double?)null : {% endif %}Convert.ToDouble(x){% elseif property.items.type == "boolean" %}{% if not property.required %}x == null ? (bool?)null : {% endif %}(bool)x{% else %}x{% endif %}){% if property.items.type == "string" and property.required %}?.Where(x => x != null){% endif %}?.ToList() ?? new List<{% if property.items.type == "string" %}string{% elseif property.items.type == "integer" %}long{% elseif property.items.type == "number" %}double{% elseif property.items.type == "boolean" %}bool{% else %}object{% endif %}>()

Copilot uses AI. Check for mistakes.

@Fellmonkey Fellmonkey force-pushed the improve-json-serialization branch from cf7eb57 to b071cbc Compare September 2, 2025 08:21
Copy link

coderabbitai bot commented Sep 2, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The converter’s Read method now returns object? and parses via JsonDocument, delegating to a new recursive ConvertElement that maps JsonElement kinds to .NET types and handles null/undefined; unsupported kinds throw JsonException. The generated Exception adds an overload accepting message and inner Exception. Model From methods switch from JsonElement-based parsing to dictionary-centric casts, add optional key guards, and use IEnumerable for arrays and numeric conversions. Role’s namespace becomes templated (spec.title). InputFile’s using is templated to {{ spec.title | caseUcfirst }}.Extensions. Several files add a trailing newline; no other public APIs change.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
templates/dotnet/Package/Query.cs.twig (1)

153-159: Validate deserialization in Or/And to avoid nulls in Values

JsonSerializer.Deserialize can return null on malformed input; currently this would silently serialize null entries. Fail fast with a clear error.

         public static string Or(List<string> queries) {
-            return new Query("or", null, queries.Select(q => JsonSerializer.Deserialize<Query>(q, Client.DeserializerOptions)).ToList()).ToString();
+            var parsed = queries
+                .Select(q => JsonSerializer.Deserialize<Query>(q, Client.DeserializerOptions) ?? throw new JsonException("Invalid query JSON in Or()"))
+                .ToList();
+            return new Query("or", null, parsed).ToString();
         }

         public static string And(List<string> queries) {
-            return new Query("and", null, queries.Select(q => JsonSerializer.Deserialize<Query>(q, Client.DeserializerOptions)).ToList()).ToString();
+            var parsed = queries
+                .Select(q => JsonSerializer.Deserialize<Query>(q, Client.DeserializerOptions) ?? throw new JsonException("Invalid query JSON in And()"))
+                .ToList();
+            return new Query("and", null, parsed).ToString();
         }
templates/dotnet/Package/Extensions/Extensions.cs.twig (1)

15-38: Correct query encoding and culture-invariant formatting

Encoding the entire string via Uri.EscapeUriString risks malformed queries and culture-specific number formats. Encode components with Uri.EscapeDataString and format numbers with InvariantCulture; normalize booleans to lowercase.

-        public static string ToQueryString(this Dictionary<string, object?> parameters)
-        {
-            var query = new List<string>();
-
-            foreach (var kvp in parameters)
-            {
-                switch (kvp.Value)
-                {
-                    case null:
-                        continue;
-                    case IList list:
-                        foreach (var item in list)
-                        {
-                            query.Add($"{kvp.Key}[]={item}");
-                        }
-                        break;
-                    default:
-                        query.Add($"{kvp.Key}={kvp.Value.ToString()}");
-                        break;
-                }
-            }
-
-            return Uri.EscapeUriString(string.Join("&", query));
-        }
+        public static string ToQueryString(this Dictionary<string, object?> parameters)
+        {
+            string Encode(object? v) =>
+                v switch
+                {
+                    null => string.Empty,
+                    bool b => b ? "true" : "false",
+                    IFormattable f => f.ToString(null, System.Globalization.CultureInfo.InvariantCulture),
+                    _ => v.ToString() ?? string.Empty
+                };
+
+            var parts = new List<string>();
+
+            foreach (var kvp in parameters)
+            {
+                switch (kvp.Value)
+                {
+                    case null:
+                        continue;
+                    case IList list when kvp.Key is not null:
+                        foreach (var item in list)
+                        {
+                            parts.Add($"{kvp.Key}[]={Uri.EscapeDataString(Encode(item))}");
+                        }
+                        break;
+                    default:
+                        parts.Add($"{kvp.Key}={Uri.EscapeDataString(Encode(kvp.Value))}");
+                        break;
+                }
+            }
+
+            return string.Join("&", parts);
+        }
♻️ Duplicate comments (2)
templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig (1)

72-73: EOF newline restored.

Matches prior request to add terminal newline.

templates/dotnet/Package/Models/Model.cs.twig (1)

51-51: Remove null-forgiving and tame the long LINQ chain.

ToList() never returns null; the bang is unnecessary. Also, this line is hard to read—prior feedback still applies.

-                        ((IEnumerable<object>)map["{{ property.name }}"]).Select(x => {% if property.items.type == "string" %}x?.ToString(){% elseif property.items.type == "integer" %}{% if not property.required %}x == null ? (long?)null : {% endif %}Convert.ToInt64(x){% elseif property.items.type == "number" %}{% if not property.required %}x == null ? (double?)null : {% endif %}Convert.ToDouble(x){% elseif property.items.type == "boolean" %}{% if not property.required %}x == null ? (bool?)null : {% endif %}(bool)x{% else %}x{% endif %}).{% if property.items.type == "string" and property.required %}Where(x => x != null).{% endif %}ToList()!
+                        ((IEnumerable<object>)map["{{ property.name }}"]).Select(x => {% if property.items.type == "string" %}x?.ToString(){% elseif property.items.type == "integer" %}{% if not property.required %}x == null ? (long?)null : {% endif %}Convert.ToInt64(x){% elseif property.items.type == "number" %}{% if not property.required %}x == null ? (double?)null : {% endif %}Convert.ToDouble(x){% elseif property.items.type == "boolean" %}{% if not property.required %}x == null ? (bool?)null : {% endif %}(bool)x{% else %}x{% endif %}).{% if property.items.type == "string" and property.required %}Where(x => x != null).{% endif %}ToList()

Optional: mirror the safe-cast pattern from Lines 45-48 for optionals.

🧹 Nitpick comments (8)
templates/dotnet/Package/Query.cs.twig (1)

25-42: Broaden handling of enumerable values in constructor

Today only IList is expanded; IEnumerable (e.g., LINQ results, HashSet) are treated as a single value. Safely broaden to IEnumerable while avoiding strings.

         public Query(string method, string? attribute, object? values)
         {
             this.Method = method;
             this.Attribute = attribute;

-            if (values is IList valuesList)
+            if (values is IList valuesList)
             {
                 this.Values = new List<object>();
                 foreach (var value in valuesList)
                 {
                     this.Values.Add(value); // Automatically boxes if value is a value type
                 }
             }
-            else if (values != null)
+            else if (values is IEnumerable enumerable && values is not string)
+            {
+                this.Values = new List<object>();
+                foreach (var value in enumerable)
+                {
+                    this.Values.Add(value!);
+                }
+            }
+            else if (values != null)
             {
                 this.Values = new List<object> { values };
             }
         }
templates/dotnet/Package/Extensions/Extensions.cs.twig (3)

1-5: Add missing using for CultureInfo (if not already imported elsewhere)

Required by the proposed InvariantCulture formatting.

 using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Text.Json;
+using System.Globalization;

40-41: Make mappings readonly

This is a constant lookup table; prevent accidental mutation.

-        private static IDictionary<string, string> _mappings = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) {
+        private static readonly IDictionary<string, string> _mappings = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) {

607-612: Use nameof in ArgumentNullException

Minor clarity/readability improvement.

-            if (extension == null)
-            {
-                throw new ArgumentNullException("extension");
-            }
+            if (extension == null)
+            {
+                throw new ArgumentNullException(nameof(extension));
+            }
templates/dotnet/Package/Models/InputFile.cs.twig (1)

14-21: Guard against invalid paths

Optional: validate input to avoid surprising runtime errors and provide clearer messages.

-        public static InputFile FromPath(string path) => new InputFile
+        public static InputFile FromPath(string path)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+                throw new ArgumentException("Path must be a non-empty string.", nameof(path));
+            return new InputFile
         {
             Path = path,
             Filename = System.IO.Path.GetFileName(path),
             MimeType = path.GetMimeType(),
             SourceType = "path"
-        };
+        };
+        }
templates/dotnet/Package/Exception.cs.twig (1)

22-25: Constructor overload looks good; consider serialization support and parity overloads.

Nice addition. Two optional tweaks:

  • Add [Serializable] + protected (SerializationInfo, StreamingContext) ctor for Exception best practices.
  • Consider an overload that also accepts code/type/response with an inner exception if those are commonly set when wrapping.
templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig (2)

38-44: Prefer DateTimeOffset (or skip auto date coercion) to preserve offsets.

Parsing string → DateTime can lose timezone info or affect round-tripping when models expect strings. Either:

  • Try DateTimeOffset first, then DateTime, else keep string.
  • Or, keep all strings as strings (let models decide). First option shown below.
-                case JsonValueKind.String:
-                    if (element.TryGetDateTime(out DateTime datetime))
-                    {
-                        return datetime;
-                    }
-                    return element.GetString();
+                case JsonValueKind.String:
+                    if (element.TryGetDateTimeOffset(out DateTimeOffset dto))
+                    {
+                        return dto;
+                    }
+                    if (element.TryGetDateTime(out DateTime dt))
+                    {
+                        return dt;
+                    }
+                    return element.GetString();

45-51: Avoid precision loss for large/monetary numbers.

Fall back to decimal before double to preserve precision; Convert.* in models will still handle decimal.

                 case JsonValueKind.Number:
                     if (element.TryGetInt64(out long l))
                     {
                         return l;
                     }
-                    return element.GetDouble();
+                    if (element.TryGetDecimal(out decimal dec))
+                    {
+                        return dec;
+                    }
+                    return element.GetDouble();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3ad8053 and b071cbc.

📒 Files selected for processing (8)
  • templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig (1 hunks)
  • templates/dotnet/Package/Exception.cs.twig (1 hunks)
  • templates/dotnet/Package/Extensions/Extensions.cs.twig (1 hunks)
  • templates/dotnet/Package/Models/InputFile.cs.twig (2 hunks)
  • templates/dotnet/Package/Models/Model.cs.twig (1 hunks)
  • templates/dotnet/Package/Models/UploadProgress.cs.twig (1 hunks)
  • templates/dotnet/Package/Query.cs.twig (1 hunks)
  • templates/dotnet/Package/Role.cs.twig (2 hunks)
🔇 Additional comments (3)
templates/dotnet/Package/Models/UploadProgress.cs.twig (1)

1-26: LGTM — formatting only

No functional changes. Safe.

templates/dotnet/Package/Role.cs.twig (1)

1-92: LGTM — namespace templating

Namespace templating aligns with the rest of the PR; no functional changes to Role API.

templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig (1)

10-16: Good fix: eliminates recursion/StackOverflow and handles nulls cleanly.

Using JsonDocument + a single pass is safer and clearer. No issues here.

Comment on lines +42 to 48
{%- if not property.required -%}map.ContainsKey("{{ property.name }}") ? {% endif %}
{%- if property.sub_schema %}
{%- if property.type == 'array' -%}
map["{{ property.name }}"] is JsonElement jsonArray{{ loop.index }} ? jsonArray{{ loop.index }}.Deserialize<List<Dictionary<string, object>>>()!.Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() : ((IEnumerable<Dictionary<string, object>>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList()
((IEnumerable<object>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it)).ToList()
{%- else -%}
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize<Dictionary<string, object>>()! : (Dictionary<string, object>)map["{{ property.name }}"])
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)map["{{ property.name }}"])
{%- endif %}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Optional key guard misses the “present-but-null” case (can throw).

If the key exists with null, casts below will NRE/ICE. Guard for null too.

-                {%- if not property.required -%}map.ContainsKey("{{ property.name }}") ? {% endif %}
+                {%- if not property.required -%}map.ContainsKey("{{ property.name }}") && map["{{ property.name }}"] != null ? {% endif %}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{%- if not property.required -%}map.ContainsKey("{{ property.name }}") ? {% endif %}
{%- if property.sub_schema %}
{%- if property.type == 'array' -%}
map["{{ property.name }}"] is JsonElement jsonArray{{ loop.index }} ? jsonArray{{ loop.index }}.Deserialize<List<Dictionary<string, object>>>()!.Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() : ((IEnumerable<Dictionary<string, object>>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList()
((IEnumerable<object>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it)).ToList()
{%- else -%}
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize<Dictionary<string, object>>()! : (Dictionary<string, object>)map["{{ property.name }}"])
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)map["{{ property.name }}"])
{%- endif %}
{%- if not property.required -%}map.ContainsKey("{{ property.name }}") && map["{{ property.name }}"] != null ? {% endif %}
{%- if property.sub_schema %}
{%- if property.type == 'array' -%}
((IEnumerable<object>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it)).ToList()
{%- else -%}
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)map["{{ property.name }}"])
{%- endif %}
🤖 Prompt for AI Agents
In templates/dotnet/Package/Models/Model.cs.twig around lines 42 to 48, the
optional-key guard only checks map.ContainsKey("{{ property.name }}") and will
still attempt casts when the key is present but null; update the guard to also
verify the value is not null (e.g., check map.ContainsKey(...) && map["{{
property.name }}"] != null or use TryGetValue to obtain the value and test for
null) before performing any casts or Select/ToList calls, and apply this null
check to both the array and object branches so null values do not cause NRE/ICE.

Comment on lines +45 to 48
((IEnumerable<object>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it)).ToList()
{%- else -%}
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize<Dictionary<string, object>>()! : (Dictionary<string, object>)map["{{ property.name }}"])
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)map["{{ property.name }}"])
{%- endif %}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Safer casts for sub-schemas (avoid InvalidCast when value types deviate).

Use safe casts for optionals to prevent InvalidCastException when value is null/mismatched.

-                        ((IEnumerable<object>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it)).ToList()
+                        ((map["{{ property.name }}"] as IEnumerable<object>) ?? Array.Empty<object>())
+                            .Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it))
+                            .ToList()
-                        {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)map["{{ property.name }}"])
+                        (map["{{ property.name }}"] as Dictionary<string, object>) is { } obj
+                            ? {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: obj)
+                            : null

Note: The second change pairs with the Line 42 null-guard; harmless for required props, safer for optionals.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
((IEnumerable<object>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it)).ToList()
{%- else -%}
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize<Dictionary<string, object>>()! : (Dictionary<string, object>)map["{{ property.name }}"])
{{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)map["{{ property.name }}"])
{%- endif %}
((map["{{ property.name }}"] as IEnumerable<object>) ?? Array.Empty<object>())
.Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary<string, object>)it))
.ToList()
{%- else -%}
(map["{{ property.name }}"] as Dictionary<string, object>) is { } obj
? {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: obj)
: null
{%- endif %}
🤖 Prompt for AI Agents
In templates/dotnet/Package/Models/Model.cs.twig around lines 45-48, avoid
direct casts that throw InvalidCastException by using safe casts and null
guards: replace ((IEnumerable<object>)map["{{ property.name }}"]) with a safe
cast (as IEnumerable<object>) and handle null by either using ?.Select(...)
followed by ToList() or coalescing to an empty sequence before mapping;
similarly replace (Dictionary<string, object>)map["{{ property.name }}"] with a
safe cast (as Dictionary<string, object>) and only call {{ property.sub_schema |
caseUcfirst | overrideIdentifier }}.From when the result is non-null (or pass a
null-safe value), keeping the existing Line 42 null-guard pairing for optionals.

map["{{ property.name }}"].ToString()
{%- endif %}
{%- else -%}
map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString()
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Preserve ISO strings when converter produced DateTime/Offset.

If the converter inferred DateTime/Offset, ToString() becomes culture-dependent. Emit ISO 8601 for round-trip.

-                                map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString()
+                                map["{{ property.name }}"] switch
+                                {
+                                    DateTimeOffset dto => dto.ToString("O"),
+                                    DateTime dt => dt.ToUniversalTime().ToString("O"),
+                                    _ => map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString()
+                                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString()
map["{{ property.name }}"]{% if not property.required %}?{% endif %} switch
{
DateTimeOffset dto => dto.ToString("O"),
DateTime dt => dt.ToUniversalTime().ToString("O"),
_ => map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString()
}
🤖 Prompt for AI Agents
In templates/dotnet/Package/Models/Model.cs.twig around line 59, the template
calls ToString() on values that a converter may have produced as
DateTime/DateTimeOffset, which is culture-dependent; update the template to emit
an ISO 8601 round-trip representation by using ToString("o") for DateTime and
DateTimeOffset cases (respecting the existing nullability ? operator if
present). Detect the property type (e.g., property.type == "DateTime" or
property.type == "DateTimeOffset" or equivalent metadata your generator
provides) and replace ToString() with ToString("o") only for those types,
leaving other types unchanged.

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.

2 participants