Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a2aa0dd
Initial plan
Copilot Mar 25, 2026
4025e20
feat: auto-generate ConfigurationSchema.json for JSON IntelliSense
Copilot Mar 25, 2026
a1ac5df
Address code review feedback: optimize enum lookup and add comments
Copilot Mar 25, 2026
da4dd67
Add changelog entry for ConfigurationSchema.json feature
Copilot Mar 25, 2026
3541fb5
Remove Azure-specific logic from base emitter ConfigurationSchemaGene…
Copilot Mar 25, 2026
f3f72b7
Delete chronus changelog file per reviewer request
Copilot Mar 25, 2026
2c0a31e
Add regenerated ConfigurationSchema.json files for test projects
Copilot Mar 27, 2026
27184ed
Add definitions section for credential and options to ConfigurationSc…
Copilot Mar 27, 2026
be700ca
Update Check-GitChanges.ps1 to detect untracked files
Copilot Mar 27, 2026
4195716
Remove local definitions from ConfigurationSchema.json
Copilot Mar 27, 2026
e83c81d
Add local definitions for non-base types (enums) in ConfigurationSche…
Copilot Mar 27, 2026
b0f6975
Fix edge case: handle single-character enum names in definition naming
Copilot Mar 27, 2026
3de54bd
Model options type inheritance from core type via named local definit…
Copilot Mar 27, 2026
06c5dba
Add test cases for options type with additional properties (string, i…
Copilot Mar 27, 2026
1d975e4
Add descriptive assertion message for allOf count in integer property…
Copilot Mar 27, 2026
3db3324
Add test for options property with model definition and support model…
Copilot Mar 27, 2026
577eac7
Add test for model as constructor parameter in ConfigurationSchema
Copilot Mar 27, 2026
d0fd621
Address PR review feedback
JoshLove-msft Mar 27, 2026
8b61ad8
Normalize line endings and add trailing newline in generated schema
JoshLove-msft Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ Invoke-LoggedCommand "git -c core.safecrlf=false diff --ignore-space-at-eol --ex
if($LastExitCode -ne 0) {
throw "Changes detected"
}

# Check for untracked files that should have been committed (e.g. newly generated files)
$untrackedOutput = Invoke-LoggedCommand "git ls-files --others --exclude-standard -- $packageRoot"
if ($untrackedOutput) {
Write-Host "Untracked files detected:"
Write-Host $untrackedOutput
throw "Untracked files detected"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Input.Extensions;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;

namespace Microsoft.TypeSpec.Generator.ClientModel
{
/// <summary>
/// Generates a ConfigurationSchema.json file for JSON IntelliSense support in appsettings.json.
/// The schema defines well-known client names and their configuration properties.
/// Common definitions (credential, options) are inherited from the System.ClientModel base schema
/// and not duplicated here. Only additional types specific to the generated client (e.g., enums,
/// custom models) are defined locally.
/// </summary>
internal static class ConfigurationSchemaGenerator
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.

@copilot we need to handle the options type inheriting from the core type. Here is an example using Azure options https://github.com/Azure/azure-sdk-for-net/pull/57548/changes

Note we will have a separate PR to handle the Azure config schema, but we need to have the same pattern for the unbranded options.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Each client's options type is now modeled as a named local definition that inherits from the base options type using allOf. For example, SampleTypeSpecClientOptions becomes "sampleTypeSpecClientOptions": { "allOf": [{ "$ref": "#/definitions/options" }] }, and client-specific custom properties (when present) are added in a second allOf element. The client's "Options" property references this local definition via $ref. This matches the pattern from azure-sdk-for-net#57548. Fixed in 3de54bd.

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.

@copilot please add test cases where an options type has additional properties as compared to the base.

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.

@copilot we should also add a test case where an options property contains a model definition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added 3 new test cases in 06c5dba:

  • Generate_OptionsDefinition_IncludesStringProperty — options with a string additional property
  • Generate_OptionsDefinition_IncludesIntegerProperty — options with an integer additional property
  • Generate_OptionsDefinition_IncludesMultipleMixedProperties — options with string + boolean + integer additional properties

Each test verifies the allOf pattern has 2 elements (base $ref + extension object with typed properties). All 17 tests pass.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added GetJsonSchemaForModel that looks up model providers in the output library and creates local definitions with their properties (referenced via $ref). New test Generate_OptionsDefinition_IncludesModelProperty creates a RetryPolicyConfig model with MaxRetries (integer) and Delay (string) properties, then verifies the schema contains a retryPolicyConfig definition with those properties and the options extension references it via $ref. Fixed in 3db3324.

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.

@copilot let's also add a test where the model is a constructor parameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added test Generate_ConstructorParameter_IncludesModelDefinition — creates a ConnectionConfig model (with Host string and Port integer properties) as a required constructor parameter. The test verifies that:

  • The model appears as a local definitions entry (connectionConfig) with its properties
  • The model appears as a top-level client property (not under Options) with $ref to the definition

All 19 tests pass. Fixed in 577eac7.

{
internal const string DefaultSectionName = "Clients";
internal const string DefaultOptionsRef = "options";

private static readonly JsonSerializerOptions s_jsonOptions = new()
{
WriteIndented = true
};

/// <summary>
/// Generates the ConfigurationSchema.json content based on the output library's type providers.
/// Returns null if no clients with <see cref="ClientSettingsProvider"/> are found.
/// </summary>
internal static string? Generate(OutputLibrary output, string sectionName = DefaultSectionName, string optionsRef = DefaultOptionsRef)
{
var clientsWithSettings = output.TypeProviders
.OfType<ClientProvider>()
.Where(c => c.ClientSettings != null)
.ToList();

if (clientsWithSettings.Count == 0)
{
return null;
}

var schema = BuildSchema(clientsWithSettings, sectionName, optionsRef);
return JsonSerializer.Serialize(schema, s_jsonOptions).ReplaceLineEndings("\n") + "\n";
}

private static JsonObject BuildSchema(
List<ClientProvider> clients,
string sectionName,
string optionsRef)
{
// Collect local definitions for non-base types during schema generation
var localDefinitions = new Dictionary<string, JsonObject>();
var clientProperties = new JsonObject();

foreach (var client in clients)
{
var clientEntry = BuildClientEntry(client, optionsRef, localDefinitions);
clientProperties[client.Name] = clientEntry;
}

var schema = new JsonObject
{
["$schema"] = "http://json-schema.org/draft-07/schema#",
["type"] = "object",
["properties"] = new JsonObject
{
[sectionName] = new JsonObject
{
["type"] = "object",
["properties"] = clientProperties,
["additionalProperties"] = new JsonObject
{
["type"] = "object",
["description"] = "Configuration for a named client instance."
}
}
}
};

// Add local definitions only for types not covered by the base schema
if (localDefinitions.Count > 0)
{
var definitions = new JsonObject();
foreach (var (name, definition) in localDefinitions.OrderBy(kvp => kvp.Key))
{
definitions[name] = definition;
}
schema["definitions"] = definitions;
}

return schema;
}

private static JsonObject BuildClientEntry(ClientProvider client, string optionsRef, Dictionary<string, JsonObject> localDefinitions)
{
var settings = client.ClientSettings!;
var properties = new JsonObject();

// Add endpoint property (Name is already transformed by PropertyProvider construction)
if (settings.EndpointProperty != null)
{
properties[settings.EndpointProperty.Name] = BuildPropertySchema(settings.EndpointProperty, localDefinitions);
}

// Add other required parameters (raw param names need ToIdentifierName() for PascalCase)
foreach (var param in settings.OtherRequiredParams)
{
var propName = param.Name.ToIdentifierName();
properties[propName] = GetJsonSchemaForType(param.Type, localDefinitions);
}

// Add credential reference (defined in System.ClientModel base schema)
properties["Credential"] = new JsonObject
{
["$ref"] = "#/definitions/credential"
};

// Add options
properties["Options"] = BuildOptionsSchema(client, optionsRef, localDefinitions);

return new JsonObject
{
["type"] = "object",
["description"] = $"Configuration for {client.Name}.",
["properties"] = properties
};
}

private static JsonObject BuildOptionsSchema(ClientProvider client, string optionsRef, Dictionary<string, JsonObject> localDefinitions)
{
var clientOptions = client.EffectiveClientOptions;
if (clientOptions == null)
{
return new JsonObject
{
["$ref"] = $"#/definitions/{optionsRef}"
};
}

// Build a named local definition for this client's options type that inherits from the base options.
// This follows the same pattern used in the Azure emitter where client options types extend the
// core options type using allOf.
var optionsTypeName = clientOptions.Name;
var definitionName = optionsTypeName.Length > 1
? char.ToLowerInvariant(optionsTypeName[0]) + optionsTypeName.Substring(1)
: optionsTypeName.ToLowerInvariant();

if (!localDefinitions.ContainsKey(definitionName))
{
// Get client-specific option properties (public, non-version properties)
var customProperties = clientOptions.Properties
.Where(p => p.Modifiers.HasFlag(MethodSignatureModifiers.Public))
.ToList();

var allOfArray = new JsonArray
{
new JsonObject { ["$ref"] = $"#/definitions/{optionsRef}" }
};

if (customProperties.Count > 0)
{
var extensionProperties = new JsonObject();
foreach (var prop in customProperties)
{
extensionProperties[prop.Name] = GetJsonSchemaForType(prop.Type, localDefinitions);
}

allOfArray.Add(new JsonObject
{
["type"] = "object",
["properties"] = extensionProperties
});
}

localDefinitions[definitionName] = new JsonObject
{
["allOf"] = allOfArray
};
}

return new JsonObject
{
["$ref"] = $"#/definitions/{definitionName}"
};
}

private static JsonObject BuildPropertySchema(PropertyProvider property, Dictionary<string, JsonObject> localDefinitions)
{
var schema = GetJsonSchemaForType(property.Type, localDefinitions);

if (property.Description != null)
{
var descriptionText = property.Description.ToString();
if (!string.IsNullOrEmpty(descriptionText))
{
schema["description"] = descriptionText;
}
}

return schema;
}

internal static JsonObject GetJsonSchemaForType(CSharpType type, Dictionary<string, JsonObject>? localDefinitions = null)
{
// Unwrap nullable types
var effectiveType = type.IsNullable ? type.WithNullable(false) : type;

// Handle non-framework types
if (!effectiveType.IsFrameworkType)
{
if (effectiveType.IsEnum)
{
return GetJsonSchemaForEnum(effectiveType, localDefinitions);
}

return GetJsonSchemaForModel(effectiveType, localDefinitions);
}

// Handle collection types
if (effectiveType.IsList)
{
return BuildArraySchema(effectiveType, localDefinitions);
}

var frameworkType = effectiveType.FrameworkType;

if (frameworkType == typeof(string))
{
return new JsonObject { ["type"] = "string" };
}
if (frameworkType == typeof(bool))
{
return new JsonObject { ["type"] = "boolean" };
}
if (frameworkType == typeof(int) || frameworkType == typeof(long))
{
return new JsonObject { ["type"] = "integer" };
}
if (frameworkType == typeof(float) || frameworkType == typeof(double))
{
return new JsonObject { ["type"] = "number" };
}
if (frameworkType == typeof(Uri))
{
return new JsonObject { ["type"] = "string", ["format"] = "uri" };
}
if (frameworkType == typeof(TimeSpan))
{
return new JsonObject { ["type"] = "string" };
}

return new JsonObject { ["type"] = "object" };
}

private static JsonObject GetJsonSchemaForEnum(CSharpType enumType, Dictionary<string, JsonObject>? localDefinitions)
{
// Search both top-level and nested types (e.g., service version enums nested in options) in a single pass
var enumProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders
.SelectMany(t => new[] { t }.Concat(t.NestedTypes))
.OfType<EnumProvider>()
.FirstOrDefault(e => e.Type.Equals(enumType));

if (enumProvider != null)
{
var values = new JsonArray();
foreach (var member in enumProvider.EnumValues)
{
values.Add(JsonValue.Create(member.Value?.ToString()));
}

JsonObject enumSchema;
if (enumType.IsStruct)
{
// Extensible enum — use anyOf to allow known values + custom strings
enumSchema = new JsonObject
{
["anyOf"] = new JsonArray
{
new JsonObject { ["enum"] = values },
new JsonObject { ["type"] = "string" }
}
};
}
else
{
// Fixed enum
enumSchema = new JsonObject { ["enum"] = values };
}

// Register as a local definition if we're collecting them
if (localDefinitions != null)
{
var name = enumProvider.Name;
var definitionName = name.Length > 1
? char.ToLowerInvariant(name[0]) + name.Substring(1)
: name.ToLowerInvariant();
if (!localDefinitions.ContainsKey(definitionName))
{
localDefinitions[definitionName] = enumSchema;
}
return new JsonObject { ["$ref"] = $"#/definitions/{definitionName}" };
}

return enumSchema;
}

// Fallback: just string
return new JsonObject { ["type"] = "string" };
}

private static JsonObject GetJsonSchemaForModel(CSharpType modelType, Dictionary<string, JsonObject>? localDefinitions)
{
// Search for the model provider in the output library
var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders
.SelectMany(t => new[] { t }.Concat(t.NestedTypes))
.FirstOrDefault(m => m is ModelProvider && m.Type.Equals(modelType));

if (modelProvider != null)
{
var name = modelProvider.Name;
var definitionName = name.Length > 1
? char.ToLowerInvariant(name[0]) + name.Substring(1)
: name.ToLowerInvariant();

if (localDefinitions != null && !localDefinitions.ContainsKey(definitionName))
{
var modelProperties = new JsonObject();
foreach (var prop in modelProvider.Properties
.Where(p => p.Modifiers.HasFlag(MethodSignatureModifiers.Public)))
{
modelProperties[prop.Name] = GetJsonSchemaForType(prop.Type, localDefinitions);
}

var modelSchema = new JsonObject { ["type"] = "object" };
if (modelProperties.Count > 0)
{
modelSchema["properties"] = modelProperties;
}

localDefinitions[definitionName] = modelSchema;
}

if (localDefinitions != null)
{
return new JsonObject { ["$ref"] = $"#/definitions/{definitionName}" };
}
}

return new JsonObject { ["type"] = "object" };
}

private static JsonObject BuildArraySchema(CSharpType listType, Dictionary<string, JsonObject>? localDefinitions)
{
if (listType.Arguments.Count > 0)
{
return new JsonObject
{
["type"] = "array",
["items"] = GetJsonSchemaForType(listType.Arguments[0], localDefinitions)
};
}

return new JsonObject
{
["type"] = "array",
["items"] = new JsonObject { ["type"] = "string" }
};
}
}
}
Loading
Loading