-
Notifications
You must be signed in to change notification settings - Fork 350
Auto-generate ConfigurationSchema.json for JSON IntelliSense #10139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JoshLove-msft
merged 25 commits into
main
from
copilot/add-auto-generate-configuration-schema-json
Mar 31, 2026
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
a2aa0dd
Initial plan
Copilot 4025e20
feat: auto-generate ConfigurationSchema.json for JSON IntelliSense
Copilot a1ac5df
Address code review feedback: optimize enum lookup and add comments
Copilot da4dd67
Add changelog entry for ConfigurationSchema.json feature
Copilot 3541fb5
Remove Azure-specific logic from base emitter ConfigurationSchemaGene…
Copilot f3f72b7
Delete chronus changelog file per reviewer request
Copilot 2c0a31e
Add regenerated ConfigurationSchema.json files for test projects
Copilot 27184ed
Add definitions section for credential and options to ConfigurationSc…
Copilot be700ca
Update Check-GitChanges.ps1 to detect untracked files
Copilot 4195716
Remove local definitions from ConfigurationSchema.json
Copilot e83c81d
Add local definitions for non-base types (enums) in ConfigurationSche…
Copilot b0f6975
Fix edge case: handle single-character enum names in definition naming
Copilot 3de54bd
Model options type inheritance from core type via named local definit…
Copilot 06c5dba
Add test cases for options type with additional properties (string, i…
Copilot 1d975e4
Add descriptive assertion message for allOf count in integer property…
Copilot 3db3324
Add test for options property with model definition and support model…
Copilot 577eac7
Add test for model as constructor parameter in ConfigurationSchema
Copilot d0fd621
Address PR review feedback
JoshLove-msft 8b61ad8
Normalize line endings and add trailing newline in generated schema
JoshLove-msft 50acbc4
Scope untracked file check to generator/ directory to avoid false pos…
Copilot 45e3d69
Remove $schema line from generated ConfigurationSchema.json
Copilot 010e432
Update System.ClientModel dependency to 1.10.0 and bump transitive deps
Copilot c2fd456
Add JsonSchemaSegment support for standalone package NuGet packing
JoshLove-msft ac415c2
Remove manually created .targets file (will be auto-generated)
JoshLove-msft 0a2c3bd
Run Generate.ps1: add .targets files and pack items to all test projects
JoshLove-msft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
376 changes: 376 additions & 0 deletions
376
...rp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,376 @@ | ||
| // 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 | ||
| { | ||
| 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 | ||
| { | ||
| ["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" } | ||
| }; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.