diff --git a/CHANGELOG.md b/CHANGELOG.md
index a24e5cc50e..812ab68f29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added model generation for schemas used by OpenAPI 3.1 webhooks. [#6394](https://github.com/microsoft/kiota/issues/6394)
- Adds support for resolving JSON Schema 2020-12 `$dynamicRef` against schemas declaring `$dynamicAnchor`, so recursive types (e.g. `LocalizedCategory.children: LocalizedCategory[]`) generate correctly instead of degrading to `UntypedNode`. Phase 1 of [#7815](https://github.com/microsoft/kiota/issues/7815).
+- Added binding-aware `$dynamicRef` resolution for `$defs` / `$dynamicAnchor` contexts, generating distinct concrete models for each active binding and preserving typed request bodies, responses, and error responses. Phase 2 of [#7815](https://github.com/microsoft/kiota/issues/7815).
### Changed
diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs
index 1045c042e0..296dae2810 100644
--- a/src/Kiota.Builder/KiotaBuilder.cs
+++ b/src/Kiota.Builder/KiotaBuilder.cs
@@ -53,7 +53,8 @@ public partial class KiotaBuilder
///
/// Tracks dynamic scopes for unresolved $dynamicRef; thread-local because generation is parallel.
///
- private static readonly ThreadLocal> _dynamicScope = new(() => new(), trackAllValues: false);
+ private sealed record DynamicScopeFrame(IOpenApiSchema Schema, string? BindingSuffix);
+ private static readonly ThreadLocal> _dynamicScope = new(() => new(), trackAllValues: false);
internal void SetOpenApiDocument(OpenApiDocument document) => openApiDocument = document ?? throw new ArgumentNullException(nameof(document));
public KiotaBuilder(ILogger logger, GenerationConfiguration config, HttpClient client, bool useKiotaConfig = false, ISettingsManagementService? settingsManagementService = null)
@@ -732,6 +733,8 @@ public async Task CreateLanguageSourceFilesAsync(GenerationLanguage language, Co
private const string ConstructorMethodName = "constructor";
internal const string UntypedNodeName = "UntypedNode";
internal const string TrailingSlashPlaceholder = "EmptyPathSegment";
+ private const string RequestBodySuffix = "RequestBody";
+ private const string ResponseSuffix = "Response";
///
/// Create a CodeClass instance that is a request builder class for the OpenApiUrlTreeNode
///
@@ -1761,34 +1764,38 @@ private string GetModelsNamespaceNameFromReferenceId(string? referenceId)
var namespaceSuffix = lastDotIndex != -1 ? $".{referenceId[..lastDotIndex]}" : string.Empty;
return $"{modelsNamespace?.Name}{string.Join(NsNameSeparator, namespaceSuffix.Split(NsNameSeparator).Select(static x => x.CleanupSymbolName()))}";
}
- private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, CodeNamespace codeNamespace, string classNameSuffix = "", IOpenApiResponse? response = default, string typeNameForInlineSchema = "", bool isRequestBody = false)
+ private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, CodeNamespace codeNamespace, string classNameSuffix = "", IOpenApiResponse? response = default, string typeNameForInlineSchema = "", bool isRequestBody = false, string dynamicBindingSuffixContext = "")
{
- _dynamicScope.Value!.Push(schema);
+ var bindingSuffix = TryGetDynamicBindingSuffix(schema, currentNode, operation, response, isRequestBody, dynamicBindingSuffixContext);
+ _dynamicScope.Value!.Push(new(schema, bindingSuffix));
try
{
var className = string.IsNullOrEmpty(typeNameForInlineSchema) ? currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, response: response, schema: schema, requestBody: isRequestBody).CleanupSymbolName() : typeNameForInlineSchema;
+ if (bindingSuffix is not null)
+ className = $"{className}{bindingSuffix}";
var codeDeclaration = AddModelDeclarationIfDoesntExist(currentNode, operation, schema, className, codeNamespace);
return new CodeType { TypeDefinition = codeDeclaration };
}
finally { _dynamicScope.Value!.Pop(); }
}
- private CodeType CreateInheritedModelDeclarationAndType(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema, bool isViaDiscriminator = false)
+ private CodeType CreateInheritedModelDeclarationAndType(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema, bool isViaDiscriminator = false, IOpenApiResponse? response = default, string dynamicBindingSuffixContext = "")
{
return new CodeType
{
- TypeDefinition = CreateInheritedModelDeclaration(currentNode, schema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema, isViaDiscriminator),
+ TypeDefinition = CreateInheritedModelDeclaration(currentNode, schema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema, isViaDiscriminator, response, dynamicBindingSuffixContext),
};
}
- private CodeClass CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema, bool isViaDiscriminator = false)
+ private CodeClass CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema, bool isViaDiscriminator = false, IOpenApiResponse? response = default, string dynamicBindingSuffixContext = "")
{
- _dynamicScope.Value!.Push(schema);
+ var bindingSuffix = TryGetDynamicBindingSuffix(schema, currentNode, operation, response, isRequestBody, dynamicBindingSuffixContext);
+ _dynamicScope.Value!.Push(new(schema, bindingSuffix));
try
{
- return CreateInheritedModelDeclarationCore(currentNode, schema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema, isViaDiscriminator);
+ return CreateInheritedModelDeclarationCore(currentNode, schema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema, isViaDiscriminator, response, dynamicBindingSuffixContext, bindingSuffix);
}
finally { _dynamicScope.Value!.Pop(); }
}
- private CodeClass CreateInheritedModelDeclarationCore(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema, bool isViaDiscriminator = false)
+ private CodeClass CreateInheritedModelDeclarationCore(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema, bool isViaDiscriminator = false, IOpenApiResponse? response = default, string dynamicBindingSuffixContext = "", string? bindingSuffix = null)
{
var flattenedAllOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).ToArray();
var referenceId = schema.GetReferenceId();
@@ -1809,6 +1816,8 @@ private CodeClass CreateInheritedModelDeclarationCore(OpenApiUrlTreeNode current
typeNameForInlineSchema :
currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: schema, requestBody: isRequestBody)))
.CleanupSymbolName();
+ if (bindingSuffix is not null)
+ className = $"{className}{bindingSuffix}";
var codeDeclaration = (rootSchemaHasProperties, inlineSchemas, referencedSchemas, isViaDiscriminator) switch
{
// greatest parent type
@@ -1973,7 +1982,8 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, IOp
// If typeNameForInlineSchema is not null and the schema is referenced, we have most likely unwrapped a referenced schema(most likely from an AllOf/OneOf/AnyOf).
// Therefore the current type/schema is not really inlined, so invalidate the typeNameForInlineSchema and just work with the information from the schema reference.
- if (schema.IsReferencedSchema() && !string.IsNullOrEmpty(typeNameForInlineSchema))
+ // Schemas with $dynamicRef are the exception: binding resolution can produce an inline schema, so keep the caller-provided inline name.
+ if (schema.IsReferencedSchema() && string.IsNullOrEmpty(schema.DynamicRef) && !string.IsNullOrEmpty(typeNameForInlineSchema))
{
typeNameForInlineSchema = string.Empty;
}
@@ -1995,13 +2005,13 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, IOp
if (schema.IsInherited())
{
// Pass isViaDiscriminator so that we can handle the special case where this model was referenced by a discriminator and we always want to generate a base class.
- return CreateInheritedModelDeclarationAndType(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema, isViaDiscriminator);
+ return CreateInheritedModelDeclarationAndType(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema, isViaDiscriminator, responseValue, suffixForInlineSchema);
}
if (schema.IsIntersection() && schema.MergeIntersectionSchemaEntries() is IOpenApiSchema mergedSchema)
{
// multiple allOf entries that do not translate to inheritance
- return CreateModelDeclarationAndType(currentNode, mergedSchema, operation, codeNamespace, suffix, response: responseValue, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody);
+ return CreateModelDeclarationAndType(currentNode, mergedSchema, operation, codeNamespace, suffix, response: responseValue, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody, dynamicBindingSuffixContext: suffixForInlineSchema);
}
if ((schema.IsInclusiveUnion() || schema.IsExclusiveUnion()) && string.IsNullOrEmpty(schema.Format)
@@ -2025,7 +2035,7 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, IOp
if (schema.IsObjectType() || schema.HasAnyProperty() || schema.IsEnum() || schema.AdditionalProperties?.Type is not null)
{
// no inheritance or union type, often empty definitions with only additional properties are used as property bags.
- return CreateModelDeclarationAndType(currentNode, schema, operation, codeNamespace, suffix, response: responseValue, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody);
+ return CreateModelDeclarationAndType(currentNode, schema, operation, codeNamespace, suffix, response: responseValue, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody, dynamicBindingSuffixContext: suffixForInlineSchema);
}
if (schema.IsArray() &&
@@ -2053,23 +2063,37 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, IOp
// Stack enumerates innermost-first (top to bottom); reverse to walk outermost-first.
foreach (var frame in scope.Reverse())
{
- // Unwrap references to their target: ResolveDynamicAnchorInContext checks a reference's
- // own sibling $dynamicAnchor / $defs, but the anchor is usually declared on the target.
- var context = frame is OpenApiSchemaReference { Target: { } t } ? t : frame;
- if (OpenApiWorkspace.ResolveDynamicAnchorInContext(context, anchorName) is { } resolved)
+ // Try frame as-is (binding $defs), then unwrapped target.
+ if (TryResolveAnchor(frame.Schema, false) is { } result)
+ return result;
+ if (frame.Schema is OpenApiSchemaReference { Target: { } target }
+ && TryResolveAnchor(target, true) is { } result2)
+ return result2;
+
+ CodeTypeBase? TryResolveAnchor(IOpenApiSchema context, bool allowFrameReferenceFallback)
{
- // Use the resolved schema's reference ID first, fall back to the resolving frame's.
- var refId = resolved.GetReferenceId() ?? frame.GetReferenceId();
- if (!string.IsNullOrEmpty(refId))
+ if (OpenApiWorkspace.ResolveDynamicAnchorInContext(context, anchorName) is not { } resolved)
+ return null;
+ // Only target resolution can borrow the frame ref id; inline binding $defs must stay inline.
+ var refId = resolved.GetReferenceId() ?? (allowFrameReferenceFallback ? frame.Schema.GetReferenceId() : null);
+ if (string.IsNullOrEmpty(refId))
+ return CreateModelDeclarations(currentNode, resolved, operation, parentElement, suffixForInlineSchema, response, typeNameForInlineSchema, isRequestBody);
+ var className = refId.Split('/').Last().Split('.').Last().CleanupSymbolName();
+ var nsName = GetModelsNamespaceNameFromReferenceId(refId);
+ var searchNs = rootNamespace?.FindOrAddNamespace(nsName) ?? codeNamespace;
+ // Recursive: forward-reference with enclosing suffix (combined recursive + generic).
+ // Must run before GetExistingDeclaration so a bare class from a non-bound reference
+ // doesn't bypass the binding suffix.
+ if (scope.Any(f => f.Schema.GetReferenceId() == refId))
{
- var className = refId.Split('/').Last().Split('.').Last().CleanupSymbolName();
- var nsName = GetModelsNamespaceNameFromReferenceId(refId);
- var searchNs = rootNamespace?.FindOrAddNamespace(nsName) ?? codeNamespace;
- if (GetExistingDeclaration(searchNs, currentNode, className) is CodeClass existing)
- return new CodeType { TypeDefinition = existing };
- // Forward reference — the class will be created by the enclosing materialization.
- return new CodeType { Name = className };
+ var enclosingSuffix = scope.Reverse().Select(static f => f.BindingSuffix).FirstOrDefault(static s => !string.IsNullOrEmpty(s));
+ var suffixedName = enclosingSuffix is null ? className : $"{className}{enclosingSuffix}";
+ if (GetExistingDeclaration(searchNs, currentNode, suffixedName) is CodeClass existingSuffixed)
+ return new CodeType { TypeDefinition = existingSuffixed };
+ return new CodeType { Name = suffixedName };
}
+ if (GetExistingDeclaration(searchNs, currentNode, className) is CodeClass existing)
+ return new CodeType { TypeDefinition = existing };
return CreateModelDeclarations(currentNode, resolved, operation, parentElement, suffixForInlineSchema, response, typeNameForInlineSchema, isRequestBody);
}
}
@@ -2087,29 +2111,87 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, IOp
/// Extracts the bare anchor name from a $dynamicRef value (e.g. #category → category,
/// https://example.com/schema#itemType → itemType). Per JSON Schema 2020-12 §7.3.3.
///
- private static string? ExtractAnchorName(string? dynamicRef)
+ internal static string? ExtractAnchorName(string? dynamicRef)
{
if (string.IsNullOrEmpty(dynamicRef))
return null;
var hashIndex = dynamicRef.LastIndexOf('#');
return hashIndex >= 0 ? dynamicRef[(hashIndex + 1)..] : dynamicRef;
}
+ private static string? TryGetDynamicBindingSuffix(IOpenApiSchema schema, OpenApiUrlTreeNode currentNode, OpenApiOperation? operation = default, IOpenApiResponse? response = default, bool isRequestBody = false, string suffixForInlineSchema = "")
+ {
+ if (schema.Definitions is null || schema.Definitions.Count == 0)
+ return ContainsDynamicReference(schema) ? GetActiveDynamicBindingSuffix() : null;
+ var anchorSuffix = string.Empty;
+ var hasInline = false;
+ foreach (var def in schema.Definitions.OrderBy(static kv => kv.Key, StringComparer.Ordinal).Select(static kv => kv.Value))
+ {
+ if (string.IsNullOrEmpty(def.DynamicAnchor))
+ continue;
+ var refId = def.GetReferenceId();
+ if (!string.IsNullOrEmpty(refId))
+ anchorSuffix += refId.CleanupSymbolName().ToFirstCharacterUpperCase();
+ else
+ hasInline = true;
+ }
+ if (!hasInline && anchorSuffix.Length == 0)
+ return ContainsDynamicReference(schema) ? GetActiveDynamicBindingSuffix() : null;
+ var suffix = anchorSuffix;
+ if (hasInline)
+ {
+ var pathSuffix = string.Join(string.Empty, currentNode.Path
+ .Split('\\', StringSplitOptions.RemoveEmptyEntries)
+ .Select(static segment => segment.CleanupSymbolName().ToFirstCharacterUpperCase()));
+ if (!string.IsNullOrEmpty(pathSuffix))
+ suffix += pathSuffix;
+ if (!string.IsNullOrEmpty(operation?.OperationId))
+ suffix += operation.OperationId.CleanupSymbolName().ToFirstCharacterUpperCase();
+ if (isRequestBody)
+ suffix += string.IsNullOrEmpty(operation?.OperationId) && !string.IsNullOrEmpty(suffixForInlineSchema) ?
+ suffixForInlineSchema.CleanupSymbolName().ToFirstCharacterUpperCase() :
+ RequestBodySuffix;
+ else if (response is not null)
+ suffix += string.IsNullOrEmpty(suffixForInlineSchema) ? ResponseSuffix : suffixForInlineSchema.CleanupSymbolName().ToFirstCharacterUpperCase();
+ }
+ return suffix.Length > 0 ? suffix : null;
+ }
+ private static string? GetActiveDynamicBindingSuffix() => _dynamicScope.Value?.Reverse().Select(static f => f.BindingSuffix).FirstOrDefault(static s => !string.IsNullOrEmpty(s));
+ private static bool ContainsDynamicReference(IOpenApiSchema schema, HashSet? visited = null)
+ {
+ visited ??= new(ReferenceEqualityComparer.Instance);
+ if (!visited.Add(schema))
+ return false;
+ return !string.IsNullOrEmpty(schema.DynamicRef) ||
+ schema is OpenApiSchemaReference { Target: { } target } && ContainsDynamicReference(target, visited) ||
+ schema.Items is not null && ContainsDynamicReference(schema.Items, visited) ||
+ schema.Properties?.Values.Any(x => ContainsDynamicReference(x, visited)) == true ||
+ schema.Definitions?.Values.Any(x => ContainsDynamicReference(x, visited)) == true ||
+ schema.AllOf?.Any(x => ContainsDynamicReference(x, visited)) == true ||
+ schema.AnyOf?.Any(x => ContainsDynamicReference(x, visited)) == true ||
+ schema.OneOf?.Any(x => ContainsDynamicReference(x, visited)) == true;
+ }
private CodeTypeBase CreateCollectionModelDeclaration(OpenApiUrlTreeNode currentNode, IOpenApiSchema schema, OpenApiOperation? operation, CodeNamespace codeNamespace, string typeNameForInlineSchema, bool isRequestBody)
{
- CodeTypeBase? type = GetPrimitiveType(schema.Items);
- var isEnumOrComposedCollectionType = schema.Items.IsEnum() //the collection could be an enum type so override with strong type instead of string type.
- || schema.Items.IsComposedEnum() && string.IsNullOrEmpty(schema.Items?.Format);//the collection could be a composed type with an enum type so override with strong type instead of string type.
- if ((string.IsNullOrEmpty(type?.Name)
- || isEnumOrComposedCollectionType)
- && schema.Items != null)
+ var shouldPush = schema.Definitions?.Values.Any(static d => !string.IsNullOrEmpty(d.DynamicAnchor)) == true;
+ if (shouldPush) _dynamicScope.Value!.Push(new(schema, null));
+ try
{
- var targetNamespace = GetShortestNamespace(codeNamespace, schema.Items);
- type = CreateModelDeclarations(currentNode, schema.Items, operation, targetNamespace, string.Empty, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody: isRequestBody);
+ CodeTypeBase? type = GetPrimitiveType(schema.Items);
+ var isEnumOrComposedCollectionType = schema.Items.IsEnum()
+ || schema.Items.IsComposedEnum() && string.IsNullOrEmpty(schema.Items?.Format);
+ if ((string.IsNullOrEmpty(type?.Name)
+ || isEnumOrComposedCollectionType)
+ && schema.Items != null)
+ {
+ var targetNamespace = GetShortestNamespace(codeNamespace, schema.Items);
+ type = CreateModelDeclarations(currentNode, schema.Items, operation, targetNamespace, string.Empty, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody: isRequestBody);
+ }
+ if (type is null)
+ return new CodeType { Name = UntypedNodeName, IsExternal = true };
+ type.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Complex;
+ return type;
}
- if (type is null)
- return new CodeType { Name = UntypedNodeName, IsExternal = true };
- type.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Complex;
- return type;
+ finally { if (shouldPush) _dynamicScope.Value!.Pop(); }
}
private CodeElement? GetExistingDeclaration(CodeNamespace currentNamespace, OpenApiUrlTreeNode currentNode, string declarationName)
{
diff --git a/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs b/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs
index b8e7fc6fde..ec6e0fd0b5 100644
--- a/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs
+++ b/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs
@@ -436,4 +436,408 @@ public async Task ResolvesNestedDynamicScopeAsync(GenerationLanguage language)
}
Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
}
+
+ [InlineData(GenerationLanguage.CSharp)]
+ [InlineData(GenerationLanguage.Java)]
+ [InlineData(GenerationLanguage.TypeScript)]
+ [InlineData(GenerationLanguage.Go)]
+ [InlineData(GenerationLanguage.Python)]
+ [Theory]
+ public async Task ResolvesGenericBindingDynamicRefAsync(GenerationLanguage language)
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = language,
+ OpenAPIFilePath = GetAbsolutePath("generic-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "GenericBinding", language.ToString()),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "GenericBinding", language.ToString()));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("PaginatedTemplateUser", allModelText, StringComparison.Ordinal);
+ Assert.Contains("PaginatedTemplateGroup", allModelText, StringComparison.Ordinal);
+ Assert.Contains("PaginatedTemplateUserProfile", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("PaginatedTemplateUser-profile", allModelText, StringComparison.Ordinal);
+ // The bare template class must NOT exist — each binding context gets its own suffixed class.
+ Assert.DoesNotContain("class PaginatedTemplate\n", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class PaginatedTemplate ", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class PaginatedTemplate:", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("type PaginatedTemplate struct", allModelText, StringComparison.Ordinal);
+ switch (language)
+ {
+ case GenerationLanguage.CSharp:
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.Java:
+ Assert.Contains("getCollectionOfObjectValues(User::createFromDiscriminatorValue)", allModelText, StringComparison.Ordinal);
+ Assert.Contains("getCollectionOfObjectValues(Group::createFromDiscriminatorValue)", allModelText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.TypeScript:
+ Assert.Contains("getCollectionOfObjectValues(createUserFromDiscriminatorValue)", allModelText, StringComparison.Ordinal);
+ Assert.Contains("getCollectionOfObjectValues(createGroupFromDiscriminatorValue)", allModelText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.Go:
+ Assert.Contains("GetCollectionOfObjectValues(CreateUserFromDiscriminatorValue)", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues(CreateGroupFromDiscriminatorValue)", allModelText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.Python:
+ Assert.Contains("n.get_collection_of_object_values(User)", allModelText, StringComparison.Ordinal);
+ Assert.Contains("n.get_collection_of_object_values(Group)", allModelText, StringComparison.Ordinal);
+ break;
+ default:
+ throw new Exception($"Please implement a test-case for {language}");
+ }
+ }
+
+ [InlineData(GenerationLanguage.CSharp)]
+ [InlineData(GenerationLanguage.Java)]
+ [InlineData(GenerationLanguage.TypeScript)]
+ [InlineData(GenerationLanguage.Go)]
+ [InlineData(GenerationLanguage.Python)]
+ [Theory]
+ public async Task ResolvesInlineBindingDynamicRefAsync(GenerationLanguage language)
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = language,
+ OpenAPIFilePath = GetAbsolutePath("inline-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "InlineBinding", language.ToString()),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "InlineBinding", language.ToString()));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ // Inline bindings (no $ref) use the route path as suffix so repeated terminal segments do not collide.
+ Assert.Contains("PaginatedTemplateUsers", allModelText, StringComparison.Ordinal);
+ Assert.Contains("PaginatedTemplateGroups", allModelText, StringComparison.Ordinal);
+ Assert.Contains("PaginatedTemplateOrgsUsers", allModelText, StringComparison.Ordinal);
+ Assert.Contains("PaginatedTemplateTeamsUsers", allModelText, StringComparison.Ordinal);
+ if (language is GenerationLanguage.CSharp)
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ }
+
+ [InlineData(GenerationLanguage.CSharp)]
+ [InlineData(GenerationLanguage.Java)]
+ [InlineData(GenerationLanguage.TypeScript)]
+ [InlineData(GenerationLanguage.Go)]
+ [InlineData(GenerationLanguage.Python)]
+ [Theory]
+ public async Task ResolvesRecursiveGenericBindingDynamicRefAsync(GenerationLanguage language)
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = language,
+ OpenAPIFilePath = GetAbsolutePath("recursive-generic-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "RecursiveGenericBinding", language.ToString()),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "RecursiveGenericBinding", language.ToString()));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ // Generic binding: distinct classes per bound type.
+ Assert.Contains("TreeTemplateBranch", allModelText, StringComparison.Ordinal);
+ Assert.Contains("TreeTemplateLeaf", allModelText, StringComparison.Ordinal);
+ // Recursive forward reference must use the suffixed name, not the bare template name.
+ Assert.DoesNotContain("class TreeTemplate\n", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class TreeTemplate ", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class TreeTemplate:", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("type TreeTemplate struct", allModelText, StringComparison.Ordinal);
+ }
+
+ [InlineData(GenerationLanguage.CSharp)]
+ [InlineData(GenerationLanguage.Java)]
+ [InlineData(GenerationLanguage.TypeScript)]
+ [InlineData(GenerationLanguage.Go)]
+ [InlineData(GenerationLanguage.Python)]
+ [Theory]
+ public async Task ResolvesRequestBodyGenericBindingDynamicRefAsync(GenerationLanguage language)
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = language,
+ OpenAPIFilePath = GetAbsolutePath("request-body-generic-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "RequestBodyGenericBinding", language.ToString()),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "RequestBodyGenericBinding", language.ToString()));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("SearchTemplateUser", allModelText, StringComparison.Ordinal);
+ Assert.Contains("SearchTemplateGroup", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class SearchTemplate\n", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class SearchTemplate ", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class SearchTemplate:", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("type SearchTemplate struct", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task ResolvesInheritedGenericBindingDynamicRefAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("inherited-generic-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "InheritedGenericBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "InheritedGenericBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class InheritedTemplateUser", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class InheritedTemplateGroup", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task DisambiguatesRequestResponseInlineBindingDynamicRefAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("request-response-inline-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "RequestResponseInlineBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "RequestResponseInlineBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("partial class SharedTemplateSharedSubmitSharedRequestBody :", allModelText, StringComparison.Ordinal);
+ Assert.Contains("partial class SharedTemplateSharedSubmitShared :", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task DisambiguatesMultipleErrorInlineBindingDynamicRefAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("multi-error-inline-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "MultiErrorInlineBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "MultiErrorInlineBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("partial class ErrorTemplateSharedErrorsGetSharedErrorsFourZeroZeroError :", allModelText, StringComparison.Ordinal);
+ Assert.Contains("partial class ErrorTemplateSharedErrorsGetSharedErrorsFourZeroFourError :", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ }
+
+ [InlineData(GenerationLanguage.CSharp)]
+ [InlineData(GenerationLanguage.Java)]
+ [InlineData(GenerationLanguage.TypeScript)]
+ [InlineData(GenerationLanguage.Go)]
+ [InlineData(GenerationLanguage.Python)]
+ [Theory]
+ public async Task ResolvesMultiAnchorGenericBindingDynamicRefAsync(GenerationLanguage language)
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = language,
+ OpenAPIFilePath = GetAbsolutePath("multi-anchor-generic-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "MultiAnchorGenericBinding", language.ToString()),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "MultiAnchorGenericBinding", language.ToString()));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ // Two anchors bound per route: dataType + errorType → suffix includes both bound type names.
+ Assert.Contains("EnvelopeTemplateUserProblemDetails", allModelText, StringComparison.Ordinal);
+ Assert.Contains("EnvelopeTemplateGroupProblemDetails", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class EnvelopeTemplate\n", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class EnvelopeTemplate ", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class EnvelopeTemplate:", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("type EnvelopeTemplate struct", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task GeneratesUntypedNodeForUnresolvedDynamicRefAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("unresolved-dynamicref.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "UnresolvedDynamicRef", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "UnresolvedDynamicRef", "CSharp"));
+ // Unresolved $dynamicRef must degrade to UntypedNode, not crash.
+ Assert.Contains("UntypedNode", allModelText, StringComparison.Ordinal);
+ }
+
+ [InlineData(GenerationLanguage.CSharp)]
+ [InlineData(GenerationLanguage.Java)]
+ [InlineData(GenerationLanguage.TypeScript)]
+ [InlineData(GenerationLanguage.Go)]
+ [InlineData(GenerationLanguage.Python)]
+ [Theory]
+ public async Task ResolvesArrayRootDynamicRefAsync(GenerationLanguage language)
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = language,
+ OpenAPIFilePath = GetAbsolutePath("array-root-dynamicref.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "ArrayRootDynamicRef", language.ToString()),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "ArrayRootDynamicRef", language.ToString()));
+ Assert.DoesNotContain("UntypedNode", allText, StringComparison.Ordinal);
+ switch (language)
+ {
+ case GenerationLanguage.CSharp:
+ Assert.Contains("SendCollectionAsync", allText, StringComparison.Ordinal);
+ Assert.Contains("SendCollectionAsync", allText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.Java:
+ Assert.Contains("User::createFromDiscriminatorValue", allText, StringComparison.Ordinal);
+ Assert.Contains("Group::createFromDiscriminatorValue", allText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.TypeScript:
+ Assert.Contains("createUserFromDiscriminatorValue", allText, StringComparison.Ordinal);
+ Assert.Contains("createGroupFromDiscriminatorValue", allText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.Go:
+ Assert.Contains("CreateUserFromDiscriminatorValue", allText, StringComparison.Ordinal);
+ Assert.Contains("CreateGroupFromDiscriminatorValue", allText, StringComparison.Ordinal);
+ break;
+ case GenerationLanguage.Python:
+ Assert.Contains("list[User]", allText, StringComparison.Ordinal);
+ Assert.Contains("list[Group]", allText, StringComparison.Ordinal);
+ break;
+ default:
+ throw new Exception($"Please implement a test-case for {language}");
+ }
+ }
+
+ [Fact]
+ public async Task ResolvesMultiInlineBindingDynamicRefAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("multi-inline-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "MultiInlineBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "MultiInlineBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ // Context suffix (route + operation) must appear exactly once, not duplicated per inline anchor.
+ var classDecl = "class CompositeTemplateCompositeGetComposite";
+ Assert.Contains(classDecl + " ", allModelText, StringComparison.Ordinal);
+ // If duplicated, the class name would contain the context twice — verify it doesn't.
+ Assert.DoesNotContain(classDecl + "Composite", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task ResolvesMixedAnchorBindingDynamicRefAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("mixed-anchor-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "MixedAnchorBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "MixedAnchorBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ // Mixed anchors: one $ref (aRef → User) + one inline (zInline).
+ // Ref suffixes come first, context appended once after.
+ Assert.Contains("MixedTemplateUserMixedGetMixed", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class MixedTemplate\n", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class MixedTemplate ", allModelText, StringComparison.Ordinal);
+ Assert.DoesNotContain("class MixedTemplate:", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task SpecializesInheritedComponentDynamicRefBindingsAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("inherited-component-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "InheritedComponentBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "InheritedComponentBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class BasePageUser", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class BasePageGroup", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ Assert.Contains("GetCollectionOfObjectValues", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task DisambiguatesInlineBindingsWithoutOperationIdsAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("no-operation-id-inline-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "NoOperationIdInlineBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "NoOperationIdInlineBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class SharedTemplateSharedGetRequestBody", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class SharedTemplateSharedPostRequestBody", allModelText, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task DisambiguatesNamespacedDynamicRefBindingsAsync()
+ {
+ var logger = LoggerFactory.Create(builder => { }).CreateLogger();
+ var configuration = new GenerationConfiguration
+ {
+ Language = GenerationLanguage.CSharp,
+ OpenAPIFilePath = GetAbsolutePath("namespaced-binding.yaml"),
+ OutputPath = Path.Combine(".", "Generated", "NamespacedBinding", "CSharp"),
+ CleanOutput = true,
+ };
+ await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new());
+
+ var allModelText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "NamespacedBinding", "CSharp"));
+ Assert.DoesNotContain("UntypedNode", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class PageTemplateV1User", allModelText, StringComparison.Ordinal);
+ Assert.Contains("class PageTemplateV2User", allModelText, StringComparison.Ordinal);
+ }
}
diff --git a/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj b/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj
index 896f584824..7239fbd268 100644
--- a/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj
+++ b/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj
@@ -69,9 +69,54 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
diff --git a/tests/Kiota.Builder.IntegrationTests/array-root-dynamicref.yaml b/tests/Kiota.Builder.IntegrationTests/array-root-dynamicref.yaml
new file mode 100644
index 0000000000..cfb2822322
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/array-root-dynamicref.yaml
@@ -0,0 +1,59 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Array Root Generic Binding API
+ version: 0.1.0
+paths:
+ /users/list:
+ get:
+ operationId: listUsersArray
+ responses:
+ '200':
+ description: Array of users via dynamic ref
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/User'
+ type: array
+ items:
+ $dynamicRef: '#itemType'
+ default:
+ description: Error
+ /groups/list:
+ get:
+ operationId: listGroupsArray
+ responses:
+ '200':
+ description: Array of groups via dynamic ref
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/Group'
+ type: array
+ items:
+ $dynamicRef: '#itemType'
+ default:
+ description: Error
+components:
+ schemas:
+ User:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ Group:
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
diff --git a/tests/Kiota.Builder.IntegrationTests/generic-binding.yaml b/tests/Kiota.Builder.IntegrationTests/generic-binding.yaml
new file mode 100644
index 0000000000..2099c56099
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/generic-binding.yaml
@@ -0,0 +1,89 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Generic Binding API
+ version: 0.1.0
+paths:
+ /users:
+ get:
+ operationId: listUsers
+ responses:
+ '200':
+ description: User page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/User'
+ $ref: '#/components/schemas/PaginatedTemplate'
+ default:
+ description: Error
+ /groups:
+ get:
+ operationId: listGroups
+ responses:
+ '200':
+ description: Group page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/Group'
+ $ref: '#/components/schemas/PaginatedTemplate'
+ default:
+ description: Error
+ /profiles:
+ get:
+ operationId: listProfiles
+ responses:
+ '200':
+ description: Profile page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/user-profile'
+ $ref: '#/components/schemas/PaginatedTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ User:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ Group:
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
+ user-profile:
+ type: object
+ required: [id, displayName]
+ properties:
+ id:
+ type: string
+ displayName:
+ type: string
+ PaginatedTemplate:
+ $id: https://example.com/schemas/PaginatedTemplate
+ $dynamicAnchor: itemType
+ type: object
+ required: [items]
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/inherited-component-binding.yaml b/tests/Kiota.Builder.IntegrationTests/inherited-component-binding.yaml
new file mode 100644
index 0000000000..9684b768ae
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/inherited-component-binding.yaml
@@ -0,0 +1,60 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Inherited Component Binding API
+ version: 0.1.0
+paths:
+ /pages/users:
+ get:
+ operationId: listUserPages
+ responses:
+ '200':
+ description: User page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/User'
+ $ref: '#/components/schemas/DerivedPage'
+ /pages/groups:
+ get:
+ operationId: listGroupPages
+ responses:
+ '200':
+ description: Group page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/Group'
+ $ref: '#/components/schemas/DerivedPage'
+components:
+ schemas:
+ User:
+ type: object
+ properties:
+ id:
+ type: string
+ Group:
+ type: object
+ properties:
+ id:
+ type: string
+ BasePage:
+ $dynamicAnchor: itemType
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
+ DerivedPage:
+ allOf:
+ - $ref: '#/components/schemas/BasePage'
+ - type: object
+ properties:
+ page:
+ type: integer
diff --git a/tests/Kiota.Builder.IntegrationTests/inherited-generic-binding.yaml b/tests/Kiota.Builder.IntegrationTests/inherited-generic-binding.yaml
new file mode 100644
index 0000000000..a93ee87b28
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/inherited-generic-binding.yaml
@@ -0,0 +1,72 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Inherited Generic Binding API
+ version: 0.1.0
+paths:
+ /inherited/users:
+ get:
+ operationId: listInheritedUsers
+ responses:
+ '200':
+ description: User page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/User'
+ $ref: '#/components/schemas/InheritedTemplate'
+ default:
+ description: Error
+ /inherited/groups:
+ get:
+ operationId: listInheritedGroups
+ responses:
+ '200':
+ description: Group page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/Group'
+ $ref: '#/components/schemas/InheritedTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ User:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ Group:
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
+ PageBase:
+ type: object
+ properties:
+ nextLink:
+ type: string
+ InheritedTemplate:
+ $id: https://example.com/schemas/InheritedTemplate
+ $dynamicAnchor: itemType
+ allOf:
+ - $ref: '#/components/schemas/PageBase'
+ - type: object
+ required: [items]
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/inline-binding.yaml b/tests/Kiota.Builder.IntegrationTests/inline-binding.yaml
new file mode 100644
index 0000000000..14d9b5abde
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/inline-binding.yaml
@@ -0,0 +1,105 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Inline Binding API
+ version: 0.1.0
+paths:
+ /users:
+ get:
+ operationId: listUsers
+ responses:
+ '200':
+ description: User page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ $ref: '#/components/schemas/PaginatedTemplate'
+ default:
+ description: Error
+ /groups:
+ get:
+ operationId: listGroups
+ responses:
+ '200':
+ description: Group page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
+ $ref: '#/components/schemas/PaginatedTemplate'
+ default:
+ description: Error
+ /orgs/users:
+ get:
+ operationId: listOrgUsers
+ responses:
+ '200':
+ description: Organization user page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, orgRole]
+ properties:
+ id:
+ type: string
+ orgRole:
+ type: string
+ $ref: '#/components/schemas/PaginatedTemplate'
+ default:
+ description: Error
+ /teams/users:
+ get:
+ operationId: listTeamUsers
+ responses:
+ '200':
+ description: Team user page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, teamRole]
+ properties:
+ id:
+ type: string
+ teamRole:
+ type: string
+ $ref: '#/components/schemas/PaginatedTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ PaginatedTemplate:
+ $id: https://example.com/schemas/PaginatedTemplate
+ $dynamicAnchor: itemType
+ type: object
+ required: [items]
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/mixed-anchor-binding.yaml b/tests/Kiota.Builder.IntegrationTests/mixed-anchor-binding.yaml
new file mode 100644
index 0000000000..7e5cb0bb9a
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/mixed-anchor-binding.yaml
@@ -0,0 +1,47 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Mixed Anchor Binding API
+ version: 0.1.0
+paths:
+ /mixed:
+ get:
+ operationId: getMixed
+ responses:
+ '200':
+ description: Mixed ref + inline anchors
+ content:
+ application/json:
+ schema:
+ $defs:
+ aRef:
+ $dynamicAnchor: aRef
+ $ref: '#/components/schemas/User'
+ zInline:
+ $dynamicAnchor: zInline
+ type: object
+ required: [code]
+ properties:
+ code:
+ type: integer
+ $ref: '#/components/schemas/MixedTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ User:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ MixedTemplate:
+ $id: https://example.com/schemas/MixedTemplate
+ type: object
+ required: [inline, ref]
+ properties:
+ inline:
+ $dynamicRef: '#zInline'
+ ref:
+ $dynamicRef: '#aRef'
diff --git a/tests/Kiota.Builder.IntegrationTests/multi-anchor-generic-binding.yaml b/tests/Kiota.Builder.IntegrationTests/multi-anchor-generic-binding.yaml
new file mode 100644
index 0000000000..68a8fa650f
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/multi-anchor-generic-binding.yaml
@@ -0,0 +1,81 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Multi-Anchor Generic Binding API
+ version: 0.1.0
+paths:
+ /user-messages:
+ get:
+ operationId: listUserMessages
+ responses:
+ '200':
+ description: User message envelope
+ content:
+ application/json:
+ schema:
+ $defs:
+ dataType:
+ $dynamicAnchor: dataType
+ $ref: '#/components/schemas/User'
+ errorType:
+ $dynamicAnchor: errorType
+ $ref: '#/components/schemas/ProblemDetails'
+ $ref: '#/components/schemas/EnvelopeTemplate'
+ default:
+ description: Error
+ /group-messages:
+ get:
+ operationId: listGroupMessages
+ responses:
+ '200':
+ description: Group message envelope
+ content:
+ application/json:
+ schema:
+ $defs:
+ dataType:
+ $dynamicAnchor: dataType
+ $ref: '#/components/schemas/Group'
+ errorType:
+ $dynamicAnchor: errorType
+ $ref: '#/components/schemas/ProblemDetails'
+ $ref: '#/components/schemas/EnvelopeTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ User:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ Group:
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
+ ProblemDetails:
+ type: object
+ required: [code, detail]
+ properties:
+ code:
+ type: integer
+ detail:
+ type: string
+ EnvelopeTemplate:
+ $id: https://example.com/schemas/EnvelopeTemplate
+ $dynamicAnchor: dataType
+ type: object
+ required: [data, errors]
+ properties:
+ data:
+ $dynamicRef: '#dataType'
+ errors:
+ type: array
+ items:
+ $dynamicRef: '#errorType'
diff --git a/tests/Kiota.Builder.IntegrationTests/multi-error-inline-binding.yaml b/tests/Kiota.Builder.IntegrationTests/multi-error-inline-binding.yaml
new file mode 100644
index 0000000000..a20b2532c6
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/multi-error-inline-binding.yaml
@@ -0,0 +1,55 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Multi Error Inline Binding API
+ version: 0.1.0
+paths:
+ /shared/errors:
+ get:
+ operationId: getSharedErrors
+ responses:
+ '204':
+ description: No content
+ '400':
+ description: User error
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ $ref: '#/components/schemas/ErrorTemplate'
+ '404':
+ description: Group error
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
+ $ref: '#/components/schemas/ErrorTemplate'
+components:
+ schemas:
+ ErrorTemplate:
+ $id: https://example.com/schemas/ErrorTemplate
+ $dynamicAnchor: itemType
+ type: object
+ required: [items]
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/multi-inline-binding.yaml b/tests/Kiota.Builder.IntegrationTests/multi-inline-binding.yaml
new file mode 100644
index 0000000000..1e34eccf5b
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/multi-inline-binding.yaml
@@ -0,0 +1,50 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Multi Inline Anchor Binding API
+ version: 0.1.0
+paths:
+ /composite:
+ get:
+ operationId: getComposite
+ responses:
+ '200':
+ description: Composite with two inline dynamic anchors
+ content:
+ application/json:
+ schema:
+ $defs:
+ dataType:
+ $dynamicAnchor: dataType
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ errorType:
+ $dynamicAnchor: errorType
+ type: object
+ required: [code, detail]
+ properties:
+ code:
+ type: integer
+ detail:
+ type: string
+ $ref: '#/components/schemas/CompositeTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ CompositeTemplate:
+ $id: https://example.com/schemas/CompositeTemplate
+ $dynamicAnchor: dataType
+ type: object
+ required: [data, errors]
+ properties:
+ data:
+ $dynamicRef: '#dataType'
+ errors:
+ type: array
+ items:
+ $dynamicRef: '#errorType'
diff --git a/tests/Kiota.Builder.IntegrationTests/namespaced-binding.yaml b/tests/Kiota.Builder.IntegrationTests/namespaced-binding.yaml
new file mode 100644
index 0000000000..457039253d
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/namespaced-binding.yaml
@@ -0,0 +1,53 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Namespaced Binding API
+ version: 0.1.0
+paths:
+ /v1/users:
+ get:
+ operationId: listV1Users
+ responses:
+ '200':
+ description: V1 user page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/v1.User'
+ $ref: '#/components/schemas/PageTemplate'
+ /v2/users:
+ get:
+ operationId: listV2Users
+ responses:
+ '200':
+ description: V2 user page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/v2.User'
+ $ref: '#/components/schemas/PageTemplate'
+components:
+ schemas:
+ v1.User:
+ type: object
+ properties:
+ legacyId:
+ type: string
+ v2.User:
+ type: object
+ properties:
+ id:
+ type: integer
+ PageTemplate:
+ $dynamicAnchor: itemType
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/no-operation-id-inline-binding.yaml b/tests/Kiota.Builder.IntegrationTests/no-operation-id-inline-binding.yaml
new file mode 100644
index 0000000000..4f9ce31a26
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/no-operation-id-inline-binding.yaml
@@ -0,0 +1,50 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef No Operation ID Inline Binding API
+ version: 0.1.0
+paths:
+ /shared:
+ get:
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ properties:
+ name:
+ type: string
+ $ref: '#/components/schemas/SharedTemplate'
+ responses:
+ '204':
+ description: No content
+ post:
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ properties:
+ count:
+ type: integer
+ $ref: '#/components/schemas/SharedTemplate'
+ responses:
+ '204':
+ description: No content
+components:
+ schemas:
+ SharedTemplate:
+ $dynamicAnchor: itemType
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/recursive-generic-binding.yaml b/tests/Kiota.Builder.IntegrationTests/recursive-generic-binding.yaml
new file mode 100644
index 0000000000..2cfed1a300
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/recursive-generic-binding.yaml
@@ -0,0 +1,67 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Recursive + Generic Binding API
+ version: 0.1.0
+paths:
+ /trees:
+ get:
+ operationId: listTrees
+ responses:
+ '200':
+ description: Tree page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/Branch'
+ $ref: '#/components/schemas/TreeTemplate'
+ default:
+ description: Error
+ /leaves:
+ get:
+ operationId: listLeaves
+ responses:
+ '200':
+ description: Leaf page
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/Leaf'
+ $ref: '#/components/schemas/TreeTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ Branch:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ Leaf:
+ type: object
+ required: [id, color]
+ properties:
+ id:
+ type: string
+ color:
+ type: string
+ TreeTemplate:
+ $id: https://example.com/schemas/TreeTemplate
+ $dynamicAnchor: self
+ type: object
+ required: [items, parent]
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
+ parent:
+ $dynamicRef: '#self'
diff --git a/tests/Kiota.Builder.IntegrationTests/request-body-generic-binding.yaml b/tests/Kiota.Builder.IntegrationTests/request-body-generic-binding.yaml
new file mode 100644
index 0000000000..78f7dad2a8
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/request-body-generic-binding.yaml
@@ -0,0 +1,71 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Request Body Generic Binding API
+ version: 0.1.0
+paths:
+ /users/search:
+ post:
+ operationId: searchUsers
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/User'
+ $ref: '#/components/schemas/SearchTemplate'
+ responses:
+ '200':
+ description: Search results
+ default:
+ description: Error
+ /groups/search:
+ post:
+ operationId: searchGroups
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ $ref: '#/components/schemas/Group'
+ $ref: '#/components/schemas/SearchTemplate'
+ responses:
+ '200':
+ description: Search results
+ default:
+ description: Error
+components:
+ schemas:
+ User:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ Group:
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
+ SearchTemplate:
+ $id: https://example.com/schemas/SearchTemplate
+ $dynamicAnchor: itemType
+ type: object
+ required: [query, filters]
+ properties:
+ query:
+ type: string
+ filters:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/request-response-inline-binding.yaml b/tests/Kiota.Builder.IntegrationTests/request-response-inline-binding.yaml
new file mode 100644
index 0000000000..388365e276
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/request-response-inline-binding.yaml
@@ -0,0 +1,55 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Request Response Inline Binding API
+ version: 0.1.0
+paths:
+ /shared:
+ post:
+ operationId: submitShared
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ $ref: '#/components/schemas/SharedTemplate'
+ responses:
+ '200':
+ description: Shared response
+ content:
+ application/json:
+ schema:
+ $defs:
+ itemType:
+ $dynamicAnchor: itemType
+ type: object
+ required: [id, label]
+ properties:
+ id:
+ type: string
+ label:
+ type: string
+ $ref: '#/components/schemas/SharedTemplate'
+ default:
+ description: Error
+components:
+ schemas:
+ SharedTemplate:
+ $id: https://example.com/schemas/SharedTemplate
+ $dynamicAnchor: itemType
+ type: object
+ required: [items]
+ properties:
+ items:
+ type: array
+ items:
+ $dynamicRef: '#itemType'
diff --git a/tests/Kiota.Builder.IntegrationTests/unresolved-dynamicref.yaml b/tests/Kiota.Builder.IntegrationTests/unresolved-dynamicref.yaml
new file mode 100644
index 0000000000..02e7bc6e15
--- /dev/null
+++ b/tests/Kiota.Builder.IntegrationTests/unresolved-dynamicref.yaml
@@ -0,0 +1,25 @@
+openapi: 3.1.0
+info:
+ title: DynamicRef Unresolved Anchor API
+ version: 0.1.0
+paths:
+ /items:
+ get:
+ operationId: listItems
+ responses:
+ '200':
+ description: Items with unresolved dynamic ref
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ItemContainer'
+ default:
+ description: Error
+components:
+ schemas:
+ ItemContainer:
+ type: object
+ required: [value]
+ properties:
+ value:
+ $dynamicRef: '#nonexistent'
diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderDynamicRefTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderDynamicRefTests.cs
new file mode 100644
index 0000000000..e2649ab7ff
--- /dev/null
+++ b/tests/Kiota.Builder.Tests/KiotaBuilderDynamicRefTests.cs
@@ -0,0 +1,17 @@
+using Xunit;
+
+namespace Kiota.Builder.Tests;
+
+public sealed partial class KiotaBuilderTests
+{
+ [Theory]
+ [InlineData("#category", "category")]
+ [InlineData("https://example.com/schema#itemType", "itemType")]
+ [InlineData("itemType", "itemType")]
+ [InlineData("", null)]
+ [InlineData(null, null)]
+ public void ExtractAnchorNameReturnsExpectedValue(string dynamicRef, string expected)
+ {
+ Assert.Equal(expected, KiotaBuilder.ExtractAnchorName(dynamicRef));
+ }
+}