Issue: Azure/azure-sdk-for-net#55538
When customers configure Azure SDK clients via appsettings.json, they get no IntelliSense or validation for well-known sections like Credential, Retry, and Diagnostics. ASP.NET Core customers expect this to "just work" — the same way Logging, Kestrel, and other ASP.NET sections already get IntelliSense via .NET Aspire's JsonSchemaSegment approach.
Each Azure SDK NuGet package ships a JSON Schema fragment via the MSBuild JsonSchemaSegment feature. Visual Studio automatically merges all fragments from all referenced NuGet packages into a single schema, providing IntelliSense for appsettings.json — zero customer setup required.
This is the same mechanism used by .NET Aspire (36+ component packages), YARP, AWS SDK .NET, and Steeltoe.
- Package-scoped IntelliSense — customers only see schema for packages they actually reference
- Each package owns its own schema — no central "super schema" to maintain
- Transitive — dependencies contribute their schemas automatically (e.g., referencing
Azure.Storage.Blobsbrings inAzure.Core's retry/diagnostics definitions transitively) - Composable — SDK packages use
$ref: "#/definitions/..."to reference shared definitions from dependency packages (VS merges all segments into one document before resolving internal refs) - Extensible — SDK packages can extend base definitions with
allOf(e.g., Blob addsEnableTenantDiscoveryto Azure.Core's options)
Configuration is split into two top-level keys matching the SDK layering:
Clients— For System.ClientModel-based clients (e.g., OpenAI). Common sections include Credential and Options (with SCM pipeline options like NetworkTimeout, ClientLoggingOptions).AzureClients— For Azure.Core-based clients (e.g., Storage, KeyVault, ARM). Common sections include Credential and Options with Retry and Diagnostics underneath.
This mirrors the actual class hierarchy:
System.ClientModel (Clients)
└── ClientSettings
├── Credential (CredentialSettings) — ApiKeyCredential only
│ ├── CredentialSource: "ApiKeyCredential"
│ ├── Key (blocked with custom error)
│ └── AdditionalProperties
└── Options (ClientPipelineOptions)
├── NetworkTimeout
├── EnableDistributedTracing
└── ClientLoggingOptions
├── EnableLogging
├── EnableMessageLogging / EnableMessageContentLogging
├── MessageContentSizeLimit
├── AllowedHeaderNames / AllowedQueryParameters
Azure.Identity (extends credential for AzureClients)
└── Credential — superset of SCM credential
├── CredentialSource: "ApiKeyCredential" | "AzureCliCredential" | "WorkloadIdentityCredential" | ...
├── TenantId, Subscription, ProcessTimeout, ... (when AzureCliCredential)
├── TenantId, ClientId, TokenFilePath, ... (when WorkloadIdentityCredential)
└── ... (conditional per CredentialSource)
Azure.Core (AzureClients)
└── ClientSettings
├── Credential → references Azure.Identity credential definition
└── Options (ClientOptions)
├── Retry (MaxRetries, Delay, MaxDelay, Mode, NetworkTimeout)
└── Diagnostics (ApplicationId, IsLoggingEnabled, ...)
{
"Logging": { ... }, // ← ASP.NET IntelliSense (unchanged)
"Clients": { // ← System.ClientModel clients
"MyOpenAIService": {
"Credential": {
"CredentialSource": "ApiKeyCredential" // ← IntelliSense: enum dropdown
},
"Options": {
"NetworkTimeout": "00:00:30", // ← IntelliSense: pipeline options
"ClientLoggingOptions": { ... }
}
}
},
"AzureClients": { // ← Azure.Core clients
"BlobServiceClient": { // ← IntelliSense: well-known name
"ServiceUri": "https://...", // ← IntelliSense: client-specific
"Credential": {
"CredentialSource": "ManagedIdentityCredential" // ← IntelliSense: all Azure.Identity types
},
"Options": {
"Retry": { "MaxRetries": 5 }, // ← IntelliSense: Retry properties
"Diagnostics": { "IsLoggingEnabled": true },
"EnableTenantDiscovery": true // ← IntelliSense: Blob-specific extension
}
}
}
}Each package ships two files via NuGet:
ConfigurationSchema.json— the JSON Schema fragment (at package root){PackageId}.targets— inbuildTransitive/{tfm}/, registers the schema:
<Project>
<ItemGroup>
<JsonSchemaSegment Include="$(MSBuildThisFileDirectory)..\..\ConfigurationSchema.json"
FilePathPattern="appsettings\..*json" />
</ItemGroup>
</Project>The packing logic is shared via Directory.Build.targets (like Aspire), so each package's .csproj stays minimal.
Each package only defines what it owns:
| Package | Schema Content |
|---|---|
| System.ClientModel | Clients section, scmCredential (ApiKeyCredential only), pipeline options, clientLoggingOptions definitions |
| Azure.Identity | credential definition with all 11 credential types + conditional properties per type |
| Azure.Core | AzureClients section, retry, diagnostics, azureOptions definitions |
| Azure.Storage.Blobs | BlobServiceClient well-known name under AzureClients, extends azureOptions with EnableTenantDiscovery via allOf |
| Azure.ResourceManager | ArmClient well-known name under AzureClients, extends azureOptions with AuxiliaryTenantIds via allOf |
VS merges ALL JsonSchemaSegment items from all referenced packages into one combined document before resolving $ref. This means:
- Azure.Identity ships
definitions: { credential: { ... } } - Azure.Storage.Blobs uses
"$ref": "#/definitions/credential"in itsBlobServiceClientschema - After VS merges them, the internal
$refresolves correctly
Similarly, SDK packages extend base definitions:
// In Azure.Storage.Blobs' ConfigurationSchema.json
"Options": {
"allOf": [
{ "$ref": "#/definitions/azureOptions" }, // ← from Azure.Core's segment
{
"type": "object",
"properties": {
"EnableTenantDiscovery": { "type": "boolean" } // ← Blob-specific extension
}
}
]
}When a customer's app references Azure.Storage.Blobs + Azure.Identity:
CustomerApp.csproj
├── PackageRef: Azure.Storage.Blobs
│ └── PackageRef: Azure.Core
│ └── PackageRef: System.ClientModel
├── PackageRef: Azure.Identity
VS collects 4 JsonSchemaSegment items (one from each package), merges them, and provides IntelliSense showing:
AzureClients→BlobServiceClient(from Blobs) + any-name fallback (from Core)Clients→ any-name with SCM credential + options (from SCM, transitive)- Full Azure.Identity credential with conditional properties (from Identity)
- Retry + Diagnostics (from Core)
If the customer doesn't reference Azure.ResourceManager, ArmClient does NOT appear. Add the package → ArmClient appears.
A working multi-package prototype is available on branch temp/json-schema-segment-test with 5 test packages (TestScm, TestCore, TestIdentity, TestBlob, TestArm) and a consumer app (ThirdPartyApp). See the README in that folder for setup instructions.
Key does not appear in IntelliSense in either IDE. If a user manually types "Key", both VS Code and Visual Studio show a red validation error with a custom message: "
This is achieved using an if/then + not: {} pattern with the message in both title and description fields (VS reads one, VS Code reads the other). deprecated: true was rejected because VS ignores it entirely.
Using JSON Schema if/then (draft-07), IntelliSense shows only the properties relevant to the selected CredentialSource. E.g., selecting "AzureCliCredential" shows TenantId, Subscription, ProcessTimeout, AdditionallyAllowedTenants. Selecting "ManagedIdentityCredential" shows ManagedIdentityIdType with a nested conditional for ManagedIdentityId.
Using anyOf: [{ enum: [...known values...] }, { type: string }] to provide known values as IntelliSense suggestions while accepting any custom string.
VS Code does not currently support JsonSchemaSegment. We've filed a request:
- microsoft/vscode-dotnettools#2831 — Add JsonSchemaSegment support to VS Code
Visual Studio now combines JsonSchemaSegment fragments using JSON Schema draft-07 semantics, which includes support for if/then/not keywords. This means:
- ✅ Basic property IntelliSense works (Credential, Retry, Diagnostics, client-specific properties)
- ✅
$refto shared definitions works across segments - ✅
allOfextension works (SDK-specific options compose with base options) - ✅ Enum dropdowns work (CredentialSource values)
- ✅ Conditional properties work — selecting
"CredentialSource": "AzureCliCredential"correctly showsTenantId, etc. - ✅ Key blocking works — the
if/then/notpattern correctly restricts properties per credential type
Resolved: VS updated its schema merge to draft-07 as of VS 18.5.0. All conditional IntelliSense (credential-specific properties, key blocking) now works without any schema changes.
- Add shared MSBuild infrastructure to auto-detect and pack
ConfigurationSchema.json(#56778) - Add
ConfigurationSchema.json+.targetstoSystem.ClientModelNuGet (#56779) - Add
ConfigurationSchema.json+.targetstoAzure.CoreNuGet (#56780) - Add
ConfigurationSchema.json+.targetstoAzure.IdentityNuGet (#56781) - Decide how to support custom credentials in JSON schema (#56282)
As each SDK creates its ClientSettings class:
- Add
ConfigurationSchema.jsontoAzure.Data.AppConfiguration(#56782) - Add
ConfigurationSchema.jsontoAzure.Security.KeyVault.Secrets(#56783) - Add
ConfigurationSchema.jsontoAzure.ResourceManager(ArmClient) (#56784) - (future) Each SDK adds its well-known name incrementally
- Work with VS team to update schema merge from draft-04 to draft-07 — done (VS 18.5.0)
- Work with VS Code team on JsonSchemaSegment support
- SDK author guide: how to add a
ConfigurationSchema.jsonto your package - Customer guide: what IntelliSense to expect, how to use
$schemafor custom names
We prototyped hosting a single schema file in azure-sdk-for-net and registering it in the SchemaStore catalog. This would provide both VS and VS Code IntelliSense with zero customer setup.
Why we moved away:
- No package scoping — every customer sees IntelliSense for ALL Azure SDK clients, not just the ones they reference. Adding
ArmClientto the schema means every user ofappsettings.jsonsees it, even if they don't use Azure.ResourceManager. - Central maintenance burden — one schema file in
azure-sdk-for-netmust be updated every time any SDK adds aClientSettingsclass. With 206+ client classes, this doesn't scale. - Cross-repo composition challenges — external SDKs (e.g., OpenAI) need to define their own schema sections, requiring complex cross-file
$refresolution. - Dual-schema merging doesn't work — testing confirmed that neither VS Code nor VS merge multiple SchemaStore schemas matching the same file. We'd have to modify the existing ASP.NET appsettings.json schema, coupling us to their release cadence.
The SchemaStore approach could still be used as a fallback for VS Code until JsonSchemaSegment support is added, but it would only provide common sections (Credential, Retry, Diagnostics) without per-package scoping.
We built working prototypes for both approaches. Key findings from the JsonSchemaSegment prototype:
- ✅ Multi-package segment merge works — definitions from different packages resolve via
#/definitions/...after VS merges - ✅ Transitive dependencies work — 3+ levels of package refs correctly flow schemas
- ✅ Package isolation works — unreferenced packages don't contribute schema
- ✅
allOfextension works — SDK packages extend base definitions with additional properties - ✅
$refbetween segments works — TestBlob references TestIdentity's credential definition - ✅
if/then/notworks in merged schema — VS now uses draft-07 for merge - ❌ External
$refdoesn't work — relative paths and HTTP URLs in$refdo not resolve within JsonSchemaSegment schemas. Only internal#/definitions/...references work post-merge.
Key findings from the SchemaStore prototype:
- ✅ Cross-file
$refworks for both VS Code and VS - ✅ Conditional credential properties work (draft-07
if/then) - ✅
allOfcomposition works for hybrid clients - ✅ Extensible enum (
anyOf) works - ❌ Dual-schema merging doesn't work in either IDE
⚠️ VS requires$refto#/definitions/..., not file root⚠️ $idbreaks local relative-path resolution in VS
This looks great!!!!
I would not put key in the schema. In fact, I wonder if there is a way to warn users if they do. The should be storing keys in env vars or KV.
Do schema files support includes? i.e. could we have our own file but then include it from the aspnet one?