Skip to content

Instantly share code, notes, and snippets.

@m-nash
Last active March 11, 2026 18:12
Show Gist options
  • Select an option

  • Save m-nash/54dc33e269485118b8da8d8cfa8759d0 to your computer and use it in GitHub Desktop.

Select an option

Save m-nash/54dc33e269485118b8da8d8cfa8759d0 to your computer and use it in GitHub Desktop.
JSON Schema IntelliSense for Azure SDK Client Configuration - Plan Summary

JSON Schema IntelliSense for Azure SDK Client Configuration

Issue: Azure/azure-sdk-for-net#55538

Problem

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.

Approach: JsonSchemaSegment via NuGet

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.

Key Benefits

  • 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.Blobs brings in Azure.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 adds EnableTenantDiscovery to Azure.Core's options)

Two Top-Level Sections: AzureClients and Clients

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, ...)

Example appsettings.json Experience

{
  "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
      }
    }
  }
}

How It Works

Package Structure

Each package ships two files via NuGet:

  1. ConfigurationSchema.json — the JSON Schema fragment (at package root)
  2. {PackageId}.targets — in buildTransitive/{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.

Schema Ownership

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

Cross-Package Definition Sharing

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 its BlobServiceClient schema
  • After VS merges them, the internal $ref resolves 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
      }
    }
  ]
}

Dependency Chain Example

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:

  • AzureClientsBlobServiceClient (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.

Prototype & Test

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 Design Decisions

Key Property Handling

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: "⚠️ Do NOT put API keys in appsettings.json. Use environment variables or Key Vault secrets instead. See https://aka.ms/azsdk/config/secrets".

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.

Conditional Credential Properties

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.

Extensible Enums

Using anyOf: [{ enum: [...known values...] }, { type: string }] to provide known values as IntelliSense suggestions while accepting any custom string.

Current IDE Limitations

VS Code: No JsonSchemaSegment Support (Yet)

VS Code does not currently support JsonSchemaSegment. We've filed a request:

VS: Schema Merge Now Uses Draft-07 ✅

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)
  • $ref to shared definitions works across segments
  • allOf extension works (SDK-specific options compose with base options)
  • ✅ Enum dropdowns work (CredentialSource values)
  • Conditional properties work — selecting "CredentialSource": "AzureCliCredential" correctly shows TenantId, etc.
  • Key blocking works — the if/then/not pattern 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.

Implementation Phases

Phase 1: Ship Schema in Core Packages

  • Add shared MSBuild infrastructure to auto-detect and pack ConfigurationSchema.json (#56778)
  • Add ConfigurationSchema.json + .targets to System.ClientModel NuGet (#56779)
  • Add ConfigurationSchema.json + .targets to Azure.Core NuGet (#56780)
  • Add ConfigurationSchema.json + .targets to Azure.Identity NuGet (#56781)
  • Decide how to support custom credentials in JSON schema (#56282)

Phase 2: Add Well-Known Client Names

As each SDK creates its ClientSettings class:

  • Add ConfigurationSchema.json to Azure.Data.AppConfiguration (#56782)
  • Add ConfigurationSchema.json to Azure.Security.KeyVault.Secrets (#56783)
  • Add ConfigurationSchema.json to Azure.ResourceManager (ArmClient) (#56784)
  • (future) Each SDK adds its well-known name incrementally

Phase 3: Resolve IDE Limitations

  • 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

Phase 4: Documentation & SDK Author Guide

  • SDK author guide: how to add a ConfigurationSchema.json to your package
  • Customer guide: what IntelliSense to expect, how to use $schema for custom names

Alternatives Considered

SchemaStore Approach (Rejected)

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 ArmClient to the schema means every user of appsettings.json sees it, even if they don't use Azure.ResourceManager.
  • Central maintenance burden — one schema file in azure-sdk-for-net must be updated every time any SDK adds a ClientSettings class. 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 $ref resolution.
  • 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.

Validated via Prototype

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
  • allOf extension works — SDK packages extend base definitions with additional properties
  • $ref between segments works — TestBlob references TestIdentity's credential definition
  • if/then/not works in merged schema — VS now uses draft-07 for merge
  • ❌ External $ref doesn't work — relative paths and HTTP URLs in $ref do not resolve within JsonSchemaSegment schemas. Only internal #/definitions/... references work post-merge.

Key findings from the SchemaStore prototype:

  • ✅ Cross-file $ref works for both VS Code and VS
  • ✅ Conditional credential properties work (draft-07 if/then)
  • allOf composition works for hybrid clients
  • ✅ Extensible enum (anyOf) works
  • ❌ Dual-schema merging doesn't work in either IDE
  • ⚠️ VS requires $ref to #/definitions/..., not file root
  • ⚠️ $id breaks local relative-path resolution in VS
@KrzysztofCwalina
Copy link

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?

@scottaddie
Copy link

We'll need to find an alternative to deprecated. The deprecated keyword was introduced in Draft 2019-09 (the specification after Draft 7). VS only supports up through Draft 7, while VS Code has limited support up through drafts 2019-09 and 2020-12.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment