Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
25 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
50acbc4
Scope untracked file check to generator/ directory to avoid false pos…
Copilot Mar 30, 2026
45e3d69
Remove $schema line from generated ConfigurationSchema.json
Copilot Mar 30, 2026
010e432
Update System.ClientModel dependency to 1.10.0 and bump transitive deps
Copilot Mar 31, 2026
c2fd456
Add JsonSchemaSegment support for standalone package NuGet packing
JoshLove-msft Mar 31, 2026
ac415c2
Remove manually created .targets file (will be auto-generated)
JoshLove-msft Mar 31, 2026
0a2c3bd
Run Generate.ps1: add .targets files and pack items to all test projects
JoshLove-msft Mar 31, 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,286 @@
// 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.
/// </summary>
internal static class ConfigurationSchemaGenerator
Comment thread
JoshLove-msft marked this conversation as resolved.
{
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);
}

private static JsonObject BuildSchema(
List<ClientProvider> clients,
string sectionName,
string optionsRef)
{
var clientProperties = new JsonObject();

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

return 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."
}
}
}
};
}

private static JsonObject BuildClientEntry(ClientProvider client, string optionsRef)
{
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);
}

// 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);
}

// Add credential reference
properties["Credential"] = new JsonObject
{
["$ref"] = "#/definitions/credential"
};

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

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

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

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

if (customProperties.Count == 0)
{
return new JsonObject
{
["$ref"] = $"#/definitions/{optionsRef}"
};
}

// Use allOf to extend the base options with client-specific properties
var extensionProperties = new JsonObject();
foreach (var prop in customProperties)
{
extensionProperties[prop.Name] = GetJsonSchemaForType(prop.Type);
}

return new JsonObject
{
["allOf"] = new JsonArray
{
new JsonObject { ["$ref"] = $"#/definitions/{optionsRef}" },
new JsonObject
{
["type"] = "object",
["properties"] = extensionProperties
}
}
};
}

private static JsonObject BuildPropertySchema(PropertyProvider property)
{
var schema = GetJsonSchemaForType(property.Type);

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

return schema;
}

internal static JsonObject GetJsonSchemaForType(CSharpType type)
{
// Unwrap nullable types
var effectiveType = type.IsNullable ? type.WithNullable(false) : type;

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

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

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

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)
{
// 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()));
}

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

// Fixed enum
return new JsonObject { ["enum"] = values };
}

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

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

return new JsonObject
{
["type"] = "array",
["items"] = new JsonObject { ["type"] = "string" }
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System;
using System.ClientModel;
using System.ComponentModel.Composition;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;

Expand Down Expand Up @@ -44,5 +46,17 @@ protected override void Configure()
AddMetadataReference(MetadataReference.CreateFromFile(typeof(JsonSerializer).Assembly.Location));
AddTypeToKeep(ModelReaderWriterContextDefinition.s_name, isRoot: false);
}

public override async Task WriteAdditionalFiles(string outputPath)
{
var schemaContent = ConfigurationSchemaGenerator.Generate(OutputLibrary);
if (schemaContent != null)
{
var schemaPath = Path.Combine(outputPath, "schema", "ConfigurationSchema.json");
Directory.CreateDirectory(Path.GetDirectoryName(schemaPath)!);
Comment thread
JoshLove-msft marked this conversation as resolved.
Outdated
Emitter.Info($"Writing {Path.GetFullPath(schemaPath)}");
await File.WriteAllTextAsync(schemaPath, schemaContent);
}
}
}
}
Loading
Loading