appsettings.json Environment Diff

Diff appsettings.Development.json against appsettings.Production.json. Catches the single most common cause of '500 on prod, works on my machine' in ASP.NET Core apps: a config key that exists in one environment and not the other.

By Pankaj Kumar · DevToolsHub · Last updated Jun 2026

Why I built this

This is the bug I've personally shipped to production more times than I'd like to admit: a key gets added to appsettings.Development.json during a feature branch, the feature works perfectly locally, the PR gets reviewed and merged, and nobody notices that appsettings.Production.json — or the Azure App Service configuration that overrides it — never got the same key. The app deploys cleanly. Then the first request that touches that code path throws a NullReferenceException or an OptionsValidationException in production, at 2am, paged by an on-call alert. I couldn't find a tool anywhere that just diffs two config files and tells you what's missing, so I built one.

How .NET configuration actually merges environments

ASP.NET Core's configuration system layers sources in a specific order: appsettings.json loads first, then appsettings.{Environment}.json overrides matching keys, then environment variables, then command-line arguments, then (in Azure) App Service Application Settings override everything. Critically, this is a merge, not a replace — if a key exists only in appsettings.Development.json and not in appsettings.Production.json, that key is simply absent in production. There's no error, no warning. IConfiguration["Jwt:Audience"] just returns null, and if you bound it to a non-nullable property via IOptions<T>, you get a startup or runtime exception depending on how validation is configured.

// This pattern hides the bug completely in Development:
var audience = builder.Configuration["Jwt:Audience"]
    ?? throw new InvalidOperationException("Jwt:Audience missing");

// Works fine locally because Development has the key.
// Throws on the FIRST request that hits this code path in Production
// if Production's appsettings.json never had it added.

// Strongly-typed options make it worse, not better, if you skip validation:
builder.Services.Configure<JwtOptions>(
    builder.Configuration.GetSection("Jwt"));
// JwtOptions.Audience silently becomes null — no exception at all
// until something downstream calls .Audience and gets a NullReferenceException

Adding .ValidateDataAnnotations().ValidateOnStart() to your options registration converts this into a fail-fast startup exception instead of a runtime surprise — but that only catches the bug after deployment, when the container fails to start. This tool catches it before you deploy, by diffing the two files directly.

How to use this tool

  1. Paste your appsettings.Development.json into the left editor.
  2. Paste your appsettings.Production.json into the right editor.
  3. Click Compare environments (or Ctrl+Enter).
  4. Review the colour-coded tree: red means a key exists in Development but is missing from Production — the dangerous case. yellow means a key exists only in Production (often legitimate — App Insights connection strings, prod-only feature flags). orange flags a type mismatch — same key, different JSON value kind, which usually means a binding error waiting to happen. green means the key exists in both with a matching structure.
  5. If your config has a ConnectionStrings section, this tool automatically checks every connection string for a Server/Data Source and a Database/Initial Catalog token, since a connection string missing one of those fails at runtime with an unhelpful SqlException.

Five real scenarios this catches

  1. A new JWT claim validation setting added during a feature branch. Jwt:Audience gets added to Development to test a new auth flow, the PR merges, and Production's AddJwtBearer setup throws SecurityTokenInvalidAudienceException on every request because the audience check now expects a value that was never configured in prod.
  2. A connection string missing the Database= segment after a copy-paste edit. Someone updates the server name in Production's connection string by hand in the Azure Portal and accidentally drops the database name. The app connects to the SQL Server instance fine — and then fails on the very first query with "Cannot open database requested by the login."
  3. A feature flag typed as a string in one environment and a boolean in another. "EnableNewCheckout": "true" in one file and "EnableNewCheckout": true in another bind completely differently depending on whether you're reading via IConfiguration.GetValue<bool>() (which parses strings leniently) or strict System.Text.Json deserialization (which throws).
  4. A retry count or timeout value that's a number in Dev and accidentally quoted as a string in Prod after an edit in a YAML-to-JSON conversion pipeline or a templating tool. IOptions<T> binding to an int property throws InvalidOperationException at startup once strict binding is enabled.
  5. Verifying an Azure App Service "Configuration" blade matches what's actually in source control before a migration to Bicep/ARM templates — paste the exported App Settings JSON against the in-repo appsettings.Production.json to find drift that's been accumulating from manual portal edits.

What this tool checks — and what it doesn't

This tool performs a structural diff: it compares which keys exist and what JSON type each value is (string, number, boolean, object, array, null). It does not flag different values between environments as a problem — your Development and Production connection strings are supposed to point at different databases, and your log levels are supposed to differ. The diff is colour-coded specifically to separate "this key is structurally absent" (the dangerous case) from "this value is legitimately different" (expected and ignored).

Frequently asked questions

Does this replace strongly-typed configuration validation? No — .ValidateDataAnnotations().ValidateOnStart() on your IOptions<T> registrations is still the right way to fail fast at container startup if config is wrong. This tool catches the problem earlier, before you've even deployed, by comparing the source files directly.

Why does the path use colons instead of dots? Because that's literally how IConfiguration addresses nested keys in .NET — Configuration["Jwt:Audience"], not Configuration["Jwt.Audience"]. The tree paths shown here match what you'd type in code.

Does it handle environment variable overrides or Key Vault references? No — this tool diffs the two JSON files as written. It can't see App Service Application Settings, environment variables, or Azure Key Vault references that override these files at runtime. For a full picture, export your App Service configuration and diff that against this file separately.

Is my data stored anywhere? No. Parsing and comparison happen in memory using System.Text.Json.JsonDocument for the duration of your session. Nothing is written to disk, a database, or a log file — this matters here specifically because connection strings and JWT secrets often appear in these files.

Why doesn't it diff array contents element by element? Arrays in appsettings.json are uncommon (Kestrel endpoint lists are the main example) and element-by-element diffing adds complexity without much real-world benefit for config files. Arrays are compared as a single value — present/missing and type-matched, not deep-diffed.

This tool is built with ASP.NET Core 8, Blazor Server, and System.Text.Json. It runs securely on Microsoft Azure.
Your input stays private. All processing happens in-memory on the server and is never stored, logged, or associated with your identity. Sensitive data — tokens, signing keys, and passwords — is safe to use here.
Input Section

Development config

appsettings.Development.json

Production config

appsettings.Production.json

Output Section
2 missing from Production 1 Production-only 1 type mismatches 6 identical
Connection string validation
  • Production → DefaultConnection is missing: Database / Initial Catalog
OK AllowedHosts
PROD ONLY ApplicationInsights(object in Production only)
ConnectionStrings
OK DefaultConnection
FeatureFlags
OK EnableNewCheckout
TYPE MISMATCH MaxRetryCountDev: number → Prod: string
Jwt
MISSING IN PROD Audience(string in Development only)
MISSING IN PROD ExpiryMinutes(number in Development only)
OK Issuer
Logging
LogLevel
OK Default
OK Microsoft.AspNetCore