Skip to content

Improve open api schema population#4443

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

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

Conversation

@Leinnan
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor

github-actions bot commented Dec 9, 2025

Lightbeam link

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 9, 2025

Lightbeam link

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator Author

Leinnan commented Dec 9, 2025

@DiasAtBeamable if you can send it it would be great

@github-actions
Copy link
Copy Markdown
Contributor

Lightbeam link

Copilot AI review requested due to automatic review settings February 20, 2026 15:34
Copy link
Copy Markdown
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 +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.
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.
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
Copy Markdown
Contributor

Lightbeam link

@Leinnan Leinnan marked this pull request as draft February 24, 2026 15:37
@Leinnan
Copy link
Copy Markdown
Collaborator Author

Leinnan commented Feb 24, 2026

State ATM- I would wait until @DiasAtBeamable work from here is merged: #4450
Then update this PR and mark it as ready for review.

Leinnan and others added 3 commits February 25, 2026 12:17
commit 5481294
Merge: 884c4aa 312e1ce
Author: DiasAtBeamable <gmoraes@beamable.com>
Date:   Tue Feb 24 13:53:26 2026 -0300

    Merge pull request #4450 from beamable/feature/code-gen-type-mappings

    New - Beamable Semantic Types and Custom Replacement Types

commit 312e1ce
Merge: 92b3851 884c4aa
Author: DiasAtBeamable <gmoraes@beamable.com>
Date:   Tue Feb 24 12:40:49 2026 -0300

    Merge branch 'main' into feature/code-gen-type-mappings

commit 92b3851
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Wed Jan 7 16:06:25 2026 -0300

    new: Added support to CustomReplacementTypes module on Unreal.

commit aaa28ae
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Wed Jan 7 11:33:14 2026 -0300

    change: Updated CHANGELOG.md

commit b6acf13
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Wed Jan 7 10:31:35 2026 -0300

    fix: Issue when adding or removing replacement types when the field is null

commit 39df7b0
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Mon Jan 5 15:31:31 2026 -0300

    new: Commands to manage replacement types

commit ee1c49f
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Tue Dec 16 15:13:49 2025 -0300

    change: merge conflict solve

commit 4e3bd50
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Tue Dec 16 14:31:21 2025 -0300

    change: Commit autogen files

commit 79a52d9
Merge: 66517d8 84e4184
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Tue Dec 16 14:05:41 2025 -0300

    Merge remote-tracking branch 'origin/main' into feature/code-gen-type-mappings

commit 66517d8
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Mon Dec 15 10:01:50 2025 -0300

    change: Added support to Equatable Long, string and struct to each SemanticType

commit 74dcc19
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Fri Dec 12 16:57:31 2025 -0300

    change: Updated Semantic Type to use types inherits by IBeamSemanticType instead of using an attribute.

commit c43b7ab
Author: Gabriel Moraes <gmoraes@beamable.com>
Date:   Thu Dec 11 10:27:01 2025 -0300

    new: Introduced BeamSemanticTypeAttribute and IBeamSemanticType.cs (with a few implementations)

    change: Updated OpenAPI gen to use it correctly and add the extensions needed for each case.
@github-actions
Copy link
Copy Markdown
Contributor

Lightbeam link

Leinnan and others added 3 commits March 16, 2026 15:37
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Leinnan Leinnan marked this pull request as ready for review March 16, 2026 14:38
@github-actions
Copy link
Copy Markdown
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

4 participants