Skip to content

Comments

Improve open api schema population#4443

Open
Leinnan wants to merge 13 commits intomainfrom
feature/improveOpenApiSchemaPopulation
Open

Improve open api schema population#4443
Leinnan wants to merge 13 commits intomainfrom
feature/improveOpenApiSchemaPopulation

Conversation

@Leinnan
Copy link
Collaborator

@Leinnan Leinnan commented Dec 9, 2025

Resolves #4407

Besides that it also improves OpenAPI components generation flow, so for example when we have class like MicroserviceRuntimeMetadata with array field public List<FederationComponentMetadata> federatedComponents = new List<FederationComponentMetadata>(); it detects that it is a separate class that is and stores information about the fact that the schema should include type information for FederationComponentMetadata as well. In old version the schema for the field was storing only that:

          "federatedComponents": {
            "type": "array"
          }

Which only informs that there is array of anything. New version gives information about items type and also stores schema for that field type.

@Leinnan Leinnan changed the title Feature/improve open api schema population Improve open api schema population Dec 9, 2025
@Leinnan Leinnan requested review from DiasAtBeamable and cdhanna and removed request for DiasAtBeamable December 9, 2025 14:27
Copy link
Contributor

@allister-beamable allister-beamable left a comment

Choose a reason for hiding this comment

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

The principle seems sound! I am not too familiar with the nuts and bolts of this code, so I defer to Chris and Dias for that part of the review :)

@github-actions
Copy link
Contributor

github-actions bot commented Dec 9, 2025

Lightbeam link

@github-actions
Copy link
Contributor

github-actions bot commented Dec 9, 2025

Lightbeam link

Copy link
Contributor

@DiasAtBeamable DiasAtBeamable left a comment

Choose a reason for hiding this comment

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

The code looks good to me. But I cannot confirm whether some of the changes will affect how the ClientCode is generated, primarily on the Unreal side. @PedroRauizBeamable has some Unreal project that would be good to test it. I have a sample C# Microservice Code with a lot of callables to test a bunch of types of usages. Let me know if you want it, I can send it to you.

Copy link
Contributor

Choose a reason for hiding this comment

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

Have you tested those changes in an Unreal and Unity project to see if all the ClientCode is being properly generated?

@Leinnan
Copy link
Collaborator Author

Leinnan commented Dec 9, 2025

@DiasAtBeamable if you can send it it would be great

@github-actions
Copy link
Contributor

Lightbeam link

Copilot AI review requested due to automatic review settings February 20, 2026 15:34
Copy link
Contributor

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 pull request resolves issue #4407 by improving the OpenAPI schema generation flow to prevent duplicate type additions to dictionaries and enhance schema population with complete type information for nested objects and arrays. The changes introduce a new dependency tracking mechanism using a HashSet<Type> parameter that is threaded through the schema conversion process to identify and recursively add all required type definitions.

Changes:

  • Refactored SchemaGenerator.Convert() to accept a ref HashSet<Type> requiredTypes parameter for tracking type dependencies
  • Added ToOpenApiSchemasDictionary() and TryAddMissingSchemaTypes() methods to handle schema deduplication and recursive type resolution
  • Simplified type name handling by consolidating GetGenericSanitizedFullName() and GetGenericQualifiedTypeName() into a single GetSanitizedFullName() method
  • Enhanced dictionary type detection with new IsDictionary(), IsSubclassOfRawGeneric(), and GetDictionaryTypes() helper methods
  • Added min/max constraints and improved type coverage for primitive types (char, sbyte, ushort, uint, ulong)
  • Added comprehensive documentation to MicroserviceRuntimeMetadata and FederationComponentMetadata classes
  • Fixed malformed XML documentation in ExpressionParser.cs
  • Improved CLI UX by handling directory paths in the download command
  • Enhanced SwaggerService schema merging with extension comparison and path equality checks

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
SchemaGenerator.cs Core refactoring to add dependency tracking and recursive type resolution
TypeExtensions.cs Simplified and unified type name generation methods
ServiceDocGenerator.cs Integration of new schema generation flow with dependency tracking
TypeTests.cs Updated all tests to use new API signature with requiredTypes parameter
ExtraTests.cs Added comprehensive test coverage for various type scenarios
MicroserviceRuntimeMetadata.cs Added XML documentation for all properties
UnrealSourceGenerator.cs Added fallback lookup for URL-encoded schema titles
SwaggerService.cs Enhanced schema merging and comparison logic
DownloadOpenAPICommand.cs Improved handling of directory paths as output
ExpressionParser.cs (both) Fixed malformed XML documentation tag

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +343 to +349
var das= GetDictionaryTypes(x);
return new OpenApiSchema
{
Type = "object",
AdditionalPropertiesAllowed = true,

AdditionalProperties = Convert(das.Value.ValueType, ref requiredTypes,depth - 1, sanitizeGenericType),
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Potential null reference exception. The GetDictionaryTypes method returns a nullable value, but the code accesses das.Value without checking if das is null. If GetDictionaryTypes returns null, this will throw a NullReferenceException. Add a null check or handle the case where the dictionary types cannot be determined.

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +182
/// <param name="oapiTypes"></param>
/// <param name="requiredTypes"></param>
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Incomplete documentation. The XML documentation for this method lacks detailed parameter descriptions. Add proper descriptions for the 'oapiTypes' and 'requiredTypes' parameters to explain their purpose, e.g., 'oapiTypes' is the list of types to convert to schemas, and 'requiredTypes' is a set that will be populated with types referenced but not yet converted.

Suggested change
/// <param name="oapiTypes"></param>
/// <param name="requiredTypes"></param>
/// <param name="oapiTypes">The list of OAPI types whose underlying CLR types will be converted into OpenAPI schemas.</param>
/// <param name="requiredTypes">A set, passed by reference, that will be populated with additional CLR types referenced by the generated schemas but not yet converted.</param>

Copilot uses AI. Check for mistakes.
Comment on lines +296 to +298
case { } x when x == typeof(short):
return new OpenApiSchema { Type = "integer", Format = "int32" };

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Duplicate case for typeof(short). This case is unreachable because there's already a case for typeof(short) at lines 283-284. This unreachable code should be removed.

Suggested change
case { } x when x == typeof(short):
return new OpenApiSchema { Type = "integer", Format = "int32" };

Copilot uses AI. Check for mistakes.
return type.FullName.Split('`')[0].Replace("+",".");
return type.FullName.Replace("+",".");
}

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Missing documentation. The IsBasicType extension method lacks XML documentation comments. Add documentation to explain that it checks if a type is a basic/primitive type including common value types like int, string, bool, etc.

Suggested change
/// <summary>
/// Determines whether the specified <see cref="Type"/> represents a basic or primitive value type,
/// including common types such as <see cref="System.Int32"/>, <see cref="string"/>, <see cref="bool"/>,
/// numeric types, and characters.
/// </summary>
/// <param name="t">The <see cref="Type"/> to evaluate.</param>
/// <returns>
/// <c>true</c> if the given <paramref name="t"/> is considered a basic or primitive type; otherwise, <c>false</c>.
/// </returns>

Copilot uses AI. Check for mistakes.
case Type x when x.IsAssignableTo(typeof(IList)) && x.IsGenericType:
elemType = x.GetGenericArguments()[0];
OpenApiSchema listOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, 1, true) : Convert(elemType, depth - 1);
OpenApiSchema listOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, ref requiredTypes, depth, true) : Convert(elemType, ref requiredTypes,depth - 1);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Inconsistent spacing. There's a missing space before 'depth' in the method call. The code has 'ref requiredTypes,depth' but should be 'ref requiredTypes, depth' for consistency with the rest of the codebase.

Copilot uses AI. Check for mistakes.
if(NamedOpenApiSchema.AreEqual(schema, component.Value, out var schemaDifferences))
continue;

var mergedSchema = MergeSchemasWithExtensionMerge(schema, component.Value, schemaDifferences);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Unused variable. The variable 'mergedSchema' is assigned the result of MergeSchemasWithExtensionMerge but is never used. The method modifies the original schema in-place (as seen in the implementation), but the assignment suggests the result should be used. Either remove the assignment or clarify the intent.

Suggested change
var mergedSchema = MergeSchemasWithExtensionMerge(schema, component.Value, schemaDifferences);
MergeSchemasWithExtensionMerge(schema, component.Value, schemaDifferences);

Copilot uses AI. Check for mistakes.
case { } x when x == typeof(long):
return new OpenApiSchema { Type = "integer", Format = "int64" };
case { } x when x == typeof(ulong):
return new OpenApiSchema { Type = "integer", Format = "int64", Minimum = ulong.MinValue, Maximum = ulong.MaxValue };
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Invalid minimum value for ulong. The OpenAPI specification requires minimum and maximum to be of type decimal or double, but ulong.MaxValue (18446744073709551615) exceeds the maximum value that can be accurately represented in a double (approximately 2^53 - 1 for integers). This may cause precision loss or schema validation issues. Consider omitting the Maximum constraint or documenting this limitation.

Suggested change
return new OpenApiSchema { Type = "integer", Format = "int64", Minimum = ulong.MinValue, Maximum = ulong.MaxValue };
return new OpenApiSchema { Type = "integer", Format = "int64", Minimum = 0 };

Copilot uses AI. Check for mistakes.
@@ -190,52 +237,89 @@ public static IEnumerable<Type> Traverse(Type runtimeType)
yield return runtimeType;
}

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Missing documentation. The TryAddMissingSchemaTypes method lacks XML documentation comments. Add documentation to explain that it recursively adds schema definitions for all types in the requiredTypes set and their dependencies to the OpenAPI document.

Suggested change
/// <summary>
/// Recursively adds OpenAPI schema definitions for all types in the specified
/// <paramref name="requiredTypes"/> set and any additional dependent types
/// discovered during schema generation to the provided <see cref="OpenApiDocument"/>.
/// </summary>
/// <param name="oapiDoc">
/// The <see cref="OpenApiDocument"/> whose <see cref="OpenApiComponents.Schemas"/>
/// collection will be populated with schema definitions for the required types.
/// </param>
/// <param name="requiredTypes">
/// A set of types that must have schema definitions present in the OpenAPI document.
/// Any additional types identified as dependencies while generating schemas are also
/// processed until no new schema types are required.
/// </param>
/// <returns>
/// <c>true</c> when all required types and their dependencies have been processed
/// and corresponding schema definitions have been added to the OpenAPI document.
/// </returns>

Copilot uses AI. Check for mistakes.
case Type x when x.IsArray:
var elemType = x.GetElementType();
OpenApiSchema arrayOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, 1, true) : Convert(elemType, depth - 1);
OpenApiSchema arrayOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, ref requiredTypes, depth, true) : Convert(elemType, ref requiredTypes,depth - 1);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Inconsistent spacing. There's a missing space before 'depth' in the method call. The code has 'ref requiredTypes,depth' but should be 'ref requiredTypes, depth' for consistency with the rest of the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines 273 to 349
@@ -244,21 +328,74 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti
{
Type = "object",
AdditionalPropertiesAllowed = true,
AdditionalProperties = Convert(x.GetGenericArguments()[1], depth - 1),
AdditionalProperties = Convert(x.GetGenericArguments()[1], ref requiredTypes,depth - 1, sanitizeGenericType),
Extensions = new Dictionary<string, IOpenApiExtension>
{
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAMESPACE] = new OpenApiString(runtimeType.Namespace),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAME] = new OpenApiString(runtimeType.Name),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(runtimeType.GetGenericQualifiedTypeName()),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(runtimeType.GetSanitizedFullName()),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY] = new OpenApiString(runtimeType.Assembly.GetName().Name),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY_VERSION] = new OpenApiString(runtimeType.Assembly.GetName().Version.ToString()),
[MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME] = new OpenApiString(runtimeType.GetGenericSanitizedFullName())
[MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME] = new OpenApiString(runtimeType.GetSanitizedFullName())
}
};
case Type x when IsDictionary(x):
var das= GetDictionaryTypes(x);
return new OpenApiSchema
{
Type = "object",
AdditionalPropertiesAllowed = true,

AdditionalProperties = Convert(das.Value.ValueType, ref requiredTypes,depth - 1, sanitizeGenericType),
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Inconsistent spacing. Multiple method calls have missing spaces before 'depth' parameter. The code has 'ref requiredTypes,depth' but should be 'ref requiredTypes, depth' for consistency. This pattern appears on lines 273, 275, 277, 331, and 349.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

Lightbeam link

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.

OAPI schema generation can accidentally add the same type twice to a dictionary

3 participants