JSON Schema ile Configuration'ı Doğrulamak: Editor IntelliSense, SchemaStore ve CI'da Validation
.NET binder yazım hatalı bir ayar key'ini sessizce yok sayar; JSON Schema'da additionalProperties: false bu hatayı dosyayı yazarken ve CI'da yakalar. Bookstore.Api için 2020-12 şeması yazıp SchemaStore, VS Code eşlemesi, YAML şema yorumu, check-jsonschema, ajv ve JsonSchema.Net ile doğruluyor; draft karışıklığını ve format'ın her yerde kontrol edilmediğini gerçek çıktılarla gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (JsonSchema.Net 9.7.0), check-jsonschema 0.39.0, Node.js 26.13 (ajv 8.20.0, ajv-formats 3.0.1), Docker Compose 5.1 ve actionlint 1.7.12 ile çalıştırılmıştır. Spec olarak JSON Schema 2020-12 esas alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) staging sunucusunda biri katalog sayfa boyutunu 50'ye çıkarmak için appsettings.Staging.json'ı elle düzenlemiş:
{
// staging sunucusunda elle düzenlendi
"Catalog": {
"PageSzie": 50,
"MaxPageSize": "100"
},
"Ordering": {
"InstitutionalMinQuantity": 20,
"InstitutionalDiscountRate": 0.10
},
"Shipping": {
"DefaultCountry": "no",
"Carriers": ["standard", "overnight"],
"SupportEmail": "support-at-bookstore.example"
}
}
Bookstore.Api bu dosyayla açılır mı? Açılırsa katalog sayfa boyutu kaç olur: 50 mi, base dosyadaki 20 mi? "100" string'i int bir property'ye bind edilir mi? Dosyadaki diğer üç hatayı kim, ne zaman fark eder?
Bind Etmek Doğrulamak Değildir
Ayarları okuyan kod şu:
#:package Microsoft.Extensions.Configuration.Json@10.0.15
#:package Microsoft.Extensions.Configuration.Binder@10.0.15
#:property PublishAot=false
using Microsoft.Extensions.Configuration;
IConfigurationRoot config = new ConfigurationBuilder()
.AddJsonFile(Path.GetFullPath("../../bookstore/src/Bookstore.Api/appsettings.json"))
.AddJsonFile(Path.GetFullPath("appsettings.Staging.json"))
.Build();
// varsayılan binding: bilinmeyen key'ler yok sayılır, "100" int'e çevrilir
CatalogOptions catalog = config.GetSection("Catalog").Get<CatalogOptions>()!;
Console.WriteLine($"PageSize={catalog.PageSize}, MaxPageSize={catalog.MaxPageSize}");
// binder, karşılığı olan bir property'si bulunmayan key'leri reddedebilir
try
{
config.GetSection("Catalog").Get<CatalogOptions>(o => o.ErrorOnUnknownConfiguration = true);
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
class CatalogOptions
{
public int PageSize { get; set; } = 20;
public int MaxPageSize { get; set; } = 100;
}
$ dotnet run bind.cs
PageSize=20, MaxPageSize=100
'ErrorOnUnknownConfiguration' was set on the provided BinderOptions, but the following properties were not found on the instance of CatalogOptions: 'PageSzie'
Uygulama hatasız açıldı ve sayfa boyutu 20 kaldı. Binder, PageSzie diye bir property bulamadığı için o key'i görmezden geldi; configuration'da her değer string olduğundan "100"'ü de sorunsuz int'e çevirdi. Staging'i düzenleyen kişi değişikliğin çalıştığını sanıyor. ErrorOnUnknownConfiguration bilinmeyen key'i yakalıyor ama ancak uygulama açılırken ve yalnızca bind edilen bölümde; no gibi küçük harfli bir ülke kodunu, listede olmayan bir kargo tipini ya da e-posta biçimine uymayan bir değeri yakalamıyor. Bu kontrolleri uygulama koduna ValidateOnStart ve DataAnnotations ile eklemek mümkün (ASP.NET Core Web API serisinin configuration yazısında). Ama hata yine deploy anında ortaya çıkar. Daha erken, dosyayı yazarken görmek için bir şemaya ihtiyacın var.
JSON Schema 2020-12'nin Temelleri
JSON Schema, bir JSON dokümanının yapısını yine JSON ile tarif eden bir dil. Güncel sürüm 2020-12; şema, hangi sürüme göre yazıldığını $schema ile söylüyor. Bookstore.Api'nin kendi ayar bölümleri için yazdığımız şema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bookstore.example/schema/bookstore-settings.schema.json",
"title": "Bookstore.Api settings",
"description": "The Bookstore-specific sections of appsettings*.json",
"type": "object",
"properties": {
"Catalog": { "$ref": "#/$defs/catalog" },
"Ordering": { "$ref": "#/$defs/ordering" },
"Shipping": { "$ref": "#/$defs/shipping" }
},
"$defs": {
"catalog": {
"type": "object",
"properties": {
"PageSize": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 },
"MaxPageSize": { "type": "integer", "minimum": 1, "maximum": 500 }
},
"additionalProperties": false
},
"ordering": {
"type": "object",
"properties": {
"InstitutionalMinQuantity": { "type": "integer", "minimum": 1 },
"InstitutionalDiscountRate": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.5 }
},
"additionalProperties": false
},
"shipping": {
"type": "object",
"properties": {
"DefaultCountry": { "type": "string", "pattern": "^[A-Z]{2}$", "description": "ISO 3166-1 alpha-2 country code" },
"Cutoff": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$" },
"Express": { "type": "boolean" },
"MaxWeightGrams": { "type": "integer", "minimum": 1 },
"ApiVersion": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+$" },
"Carriers": {
"type": "array",
"items": { "enum": ["standard", "express"] },
"uniqueItems": true
},
"SupportEmail": { "type": "string", "format": "email" }
},
"additionalProperties": false
}
}
}
En sık kullanacağın keyword'ler bu dosyada. type değerin tipini, properties bir object'in alanlarını, enum izin verilen değerleri, pattern bir string'in uyması gereken regular expression'ı, minimum/maximum sayı aralığını söylüyor. $defs şemanın içinde tekrar kullanılabilir parçalar tanımlıyor, $ref onlara işaret ediyor. additionalProperties: false ise şemada olmayan her key'i hata sayıyor; yazım hatalarını yakalayan keyword bu. required bilinçli olarak yok: appsettings.Staging.json gibi bir katman dosyası yalnızca değişen ayarları içerir, "her dosyada PageSize olsun" kuralı katmanlamayı bozar. Zorunlu alan kontrolü birleşmiş configuration üzerinde, uygulamanın açılışında yapılmalı.
format özel bir durum. 2020-12'de format varsayılan olarak yalnızca bir annotation, yani bilgi amaçlı; doğrulayıcının e-posta biçimini kontrol etmesi zorunlu değil. Birazdan göreceğimiz gibi araçlar bu konuda farklı davranıyor. Şemanın kendisinin geçerli olduğunu ve staging dosyasını check-jsonschema ile kontrol edelim. appsettings*.json dosyaları JSONC (yorum ve trailing comma'ya izin veren JSON) olduğu için --force-filetype json5 veriyoruz; JSON5'in (JSON'un JavaScript syntax'ına yaklaşan genişletilmiş hâli) parser'ı JSONC'yi de okuyor:
$ check-jsonschema --check-metaschema ../../bookstore/schema/bookstore-settings.schema.json
ok -- validation done
$ check-jsonschema --force-filetype json5 --schemafile ../../bookstore/schema/bookstore-settings.schema.json appsettings.Staging.json
Schema validation errors were encountered.
appsettings.Staging.json::$.Catalog.MaxPageSize: '100' is not of type 'integer'
appsettings.Staging.json::$.Catalog: Additional properties are not allowed ('PageSzie' was unexpected)
appsettings.Staging.json::$.Shipping.DefaultCountry: 'no' does not match '^[A-Z]{2}$'
appsettings.Staging.json::$.Shipping.Carriers[1]: 'overnight' is not one of ['standard', 'express']
appsettings.Staging.json::$.Shipping.SupportEmail: 'support-at-bookstore.example' is not a 'email'
Beş hatanın beşi de yakalandı ve her birinin JSON path'i belli. Binder'ın sessizce geçtiği yazım hatası burada açık bir mesaj.
SchemaStore ve Editor'de Tamamlama
JSON Schema'nın asıl kazancı editor'de. SchemaStore, yaygın config dosyaları için yazılmış şemaların açık bir kataloğu; Visual Studio, Visual Studio Code, Rider ve diğer JetBrains IDE'leri bu kataloğu kullanıyor. appsettings.json, package.json, tsconfig.json, compose.yaml ve GitHub Actions workflow'ları için ayrıca bir şey yapmana gerek yok: dosya adı katalogdaki desenle eşleştiği an tamamlama ve hata gösterimi gelir. Aynı şemayı komut satırında da kullanabilirsin:
$ cat appsettings.loglevel.json
{
"Logging": {
"LogLevel": {
"Default": "Informaton"
}
},
"AllowedHosts": "*"
}
$ check-jsonschema --schemafile https://json.schemastore.org/appsettings.json appsettings.loglevel.json
Schema validation errors were encountered.
appsettings.loglevel.json::$.Logging.LogLevel.Default: 'Informaton' is not one of ['Trace', 'Debug', 'Information', 'Warning', 'Error', 'Critical', 'None']
LogLevel değerlerinin listesi SchemaStore'daki appsettings şemasından geliyor; editor'de de aynı listeyi tamamlama olarak görürsün. Kendi şemanı editor'e tanıtmanın iki yolu var. Birincisi dosyanın içine "$schema": "..." yazmak; Visual Studio Code dokümanı bunun JSON'un kendisini değiştirdiğini ve dosyayı okuyan başka sistemlerde sorun çıkarabileceğini hatırlatıyor (.NET'te bu key configuration'a da girer). İkincisi workspace ayarlarında dosya deseniyle eşlemek; Bookstore'da bunu seçtik:
{
"json.schemas": [
{
"fileMatch": ["appsettings*.json"],
"url": "./schema/bookstore-settings.schema.json"
}
]
}
Editor bu eşlemeyle appsettings*.json dosyalarını hem SchemaStore'un appsettings şemasına hem bizimkine göre denetliyor. Visual Studio ve Rider'ın kendi şema eşleme ayarları var; bu yazı için bu iki IDE'yi çalıştırmadım, adımlar ürünlerin dokümanında.
YAML (girintiye dayalı veri formatı) tarafında Red Hat'in YAML extension'ı (yaml-language-server) SchemaStore'u kullanıyor ve dosya başına bir yorumla şema seçmene izin veriyor. Bookstore'un kargo ayarlarının YAML hâli, şema yorumuyla:
# yaml-language-server: $schema=../../bookstore/schema/bookstore-settings.schema.json
Shipping:
DefaultCountry: "NO"
Cutoff: "12:30"
Express: true
MaxWeightGrams: 1000
ApiVersion: "1.10"
Carriers:
- standard
- express
$ check-jsonschema --schemafile ../../bookstore/schema/bookstore-settings.schema.json shipping.yaml ../04-config/settings/shipping.yaml
Schema validation errors were encountered.
../04-config/settings/shipping.yaml::$.Shipping.Express: 'yes' is not of type 'boolean'
../04-config/settings/shipping.yaml::$.Shipping.ApiVersion: 1.1 is not of type 'string'
Doğru yazılmış dosya geçti (check-jsonschema geçen dosyalar için ayrıca satır yazmıyor). Serinin dördüncü yazısındaki tırnaksız YAML ise iki hata verdi: yes boolean değil string, 1.10 ise string değil sayı. Üçüncü yazıda yamllint'in yakalayamadığı 1.10'u şema yakaladı, çünkü şema değerin tipini biliyor. check-jsonschema YAML'ı 1.2 kurallarıyla okuyor; NO bu yüzden sorun çıkarmadı.
Compose ve Workflow Dosyaları İçin Hazır Şemalar
check-jsonschema, SchemaStore'daki bazı şemaları kendi içinde taşıyor; internete çıkmadan kullanabilirsin. Compose dosyasında environment'ı yanlış yazalım:
$ cat compose.typo.yaml
services:
api:
image: bookstore-api:dev
enviroment:
Catalog__PageSize: "20"
$ check-jsonschema --builtin-schema vendor.compose-spec compose.typo.yaml
Schema validation errors were encountered.
compose.typo.yaml::$.services.api: Unevaluated properties are not allowed ('enviroment' was unexpected)
$ docker compose -f compose.typo.yaml config 2>&1 | sed "s|$PWD/||"
validating compose.typo.yaml: services.api additional properties 'enviroment' not allowed
Şema ve docker compose config aynı hatayı buldu; Compose kendi doğrulamasını da aynı Compose Specification şemasıyla yapıyor. Fark, şemanın Docker kurulu olmayan bir CI adımında, saniyenin altında çalışması.
Draft'ları Karıştırmak
JSON Schema'nın sürümleri (draft-04, draft-06, draft-07, 2019-09, 2020-12) keyword'lerde farklı. Doğrulayıcı $schema'ya bakıp o sürümün keyword'lerini uyguluyor ve tanımadığı keyword'ü hata vermeden atlıyor. İnternetten kopyalanan bir draft-07 şemasına 2019-09'da gelen unevaluatedProperties'i eklediğini düşün:
$ cat catalog.draft-07.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"Catalog": {
"type": "object",
"properties": {
"PageSize": { "type": "integer" }
},
"unevaluatedProperties": false
}
}
}
$ check-jsonschema --force-filetype json5 --schemafile catalog.draft-07.schema.json appsettings.Staging.json
ok -- validation done
$ check-jsonschema --force-filetype json5 --schemafile catalog.2020-12.schema.json appsettings.Staging.json
Schema validation errors were encountered.
appsettings.Staging.json::$.Catalog: Unevaluated properties are not allowed ('MaxPageSize', 'PageSzie' were unexpected)
Aynı şema, yalnızca $schema satırı farklıyken, draft-07'de hiçbir şey yakalamadı, 2020-12'de iki fazla property'yi yakaladı. Bu tuzağın kötü yanı hata değil "ok" mesajı vermesi. Şema yazarken $schema'yı her zaman açıkça 2020-12 yap, kopyaladığın parçaların hangi sürüme ait olduğuna bak ve şemayı --check-metaschema ile kontrol et. OpenAPI 3.1 de JSON Schema 2020-12 ile hizalı; .NET 10'un ürettiği OpenAPI dokümanındaki şemalar bu yüzden aynı keyword'leri kullanıyor (ASP.NET Core Web API serisinin .NET 10 yenilikleri yazısında).
Aynı Şema, Üç Doğrulayıcı
Şemayı Node.js'te ajv ile, .NET'te JsonSchema.Net ile de çalıştırabilirsin. ajv için ayrı bir CLI paketi var (ajv-cli) ama son sürümü 2021'den; kütüphaneyi doğrudan kullanan küçük bir script daha güncel bir seçim (klasörde önce npm install):
$ node validate.mjs ../../bookstore/schema/bookstore-settings.schema.json appsettings.Staging.json
appsettings.Staging.json: /Catalog must NOT have additional properties
appsettings.Staging.json: /Catalog/MaxPageSize must be integer
appsettings.Staging.json: /Shipping/DefaultCountry must match pattern "^[A-Z]{2}$"
appsettings.Staging.json: /Shipping/Carriers/1 must be equal to one of the allowed values
appsettings.Staging.json: /Shipping/SupportEmail must match format "email"
#:package JsonSchema.Net@9.7.0
#:property PublishAot=false
using System.Text.Json;
using Json.Schema;
JsonSchema schema = JsonSchema.FromFile(Path.GetFullPath(args[0]));
// appsettings dosyaları JSONC: yorumları atla, trailing comma'ya izin ver
JsonDocumentOptions jsonc = new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true };
using JsonDocument data = JsonDocument.Parse(File.ReadAllText(args[1]), jsonc);
EvaluationResults results = schema.Evaluate(data.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List });
Console.WriteLine($"{args[1]}: {(results.IsValid ? "valid" : "invalid")}");
foreach (EvaluationResults detail in (results.Details ?? []).Where(d => d.Errors is { Count: > 0 }))
{
foreach ((string keyword, string message) in detail.Errors!)
{
Console.WriteLine($" {detail.InstanceLocation} [{keyword}] {message}");
}
}
$ dotnet run validate.cs -- ../../bookstore/schema/bookstore-settings.schema.json appsettings.Staging.json
appsettings.Staging.json: invalid
[properties] Some properties did not match the required schema: ["Catalog","Shipping"]
/Catalog [properties] Some properties did not match the required schema: ["MaxPageSize"]
/Catalog [additionalProperties] Some additional properties did not match the required schema: ["PageSzie"]
/Shipping [properties] Some properties did not match the required schema: ["DefaultCountry","Carriers"]
/Catalog/MaxPageSize [type] Value is "string" but should be "integer"
/Catalog/PageSzie [] All values fail against the false schema
/Shipping/DefaultCountry [pattern] The string value is not a match for the indicated regular expression
/Shipping/Carriers [items] Some items do not match the required schema; failing indexes: [1]
/Shipping/Carriers/1 [enum] Value should match one of the values specified by the enum
Üç doğrulayıcı aynı hataları buldu, ama iki fark var. Birincisi mesajların biçimi: check-jsonschema ve ajv düz bir liste veriyor, JsonSchema.Net her şema seviyesinde bir sonuç döndürüyor. İkincisi format: ajv, ajv-formats eklendiğinde e-postayı kontrol etti; JsonSchema.Net varsayılan ayarla etmedi, çünkü 2020-12'de format bir annotation. Bir kontrolün gerçekten yapılması gerekiyorsa format'a güvenme, pattern yaz ya da doğrulayıcının format kontrolünü açık olarak aç. .NET tarafında bu kodu bir unit test'e koymak, şemayı ve ayar dosyalarını aynı test çalıştırmasında doğrulamanın en kolay yolu.
CI'da Doğrulama
Bookstore'un CI workflow'unda lint job'ına iki adım ekledik:
name: ci
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- run: pip install yamllint==1.38.0 check-jsonschema==0.39.0 json5==0.17.3
# -f github her bulguyu pull request'te bir annotation'a çevirir
- run: yamllint -f github .
- name: Validate settings against Bookstore's schema
run: >-
check-jsonschema --force-filetype json5
--schemafile schema/bookstore-settings.schema.json
src/Bookstore.Api/appsettings*.json
- name: Validate Compose and workflow files
run: |
check-jsonschema --builtin-schema vendor.compose-spec compose.yaml
check-jsonschema --builtin-schema vendor.github-workflows .github/workflows/*.yml
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- name: Build
run: |
dotnet restore src/Bookstore.Api
dotnet build src/Bookstore.Api -c Release --no-restore
$ check-jsonschema --force-filetype json5 --schemafile schema/bookstore-settings.schema.json src/Bookstore.Api/appsettings*.json
ok -- validation done
$ check-jsonschema --builtin-schema vendor.compose-spec compose.yaml
ok -- validation done
$ check-jsonschema --builtin-schema vendor.github-workflows .github/workflows/*.yml
ok -- validation done
$ actionlint .github/workflows/ci.yml && echo "actionlint: no findings"
actionlint: no findings
Uzun komutu satırlara bölmek için >- kullandık: folded scalar satırları boşlukla birleştirdiği için tek bir komut oluyor, serinin üçüncü yazısındaki tuzağın doğru kullanımı. Paket sürümlerini sabitledik; check-jsonschema'nın içindeki SchemaStore şemaları sürümle birlikte değişir ve sabit bir sürüm, CI sonucunun bir gün sebepsiz kırmızıya dönmesini engeller.
Aynı kontrolü daha da erkene, commit anına çekmek de mümkün: check-jsonschema'nın dokümanında pre-commit framework'ü için hazır hook tanımları var. Ama hook'lar geliştiricinin makinesinde atlanabilir; CI'daki adım, kuralın herkes için uygulandığı tek yer. Bookstore'da ikisini birlikte kullanıyoruz: hook hızlı geri bildirim veriyor, CI son kararı veriyor. Staging veya production dosyalarını elle düzenleyen bir operasyon ekibi varsa onlar için de aynı komut işe yarıyor: hata mesajı hangi dosyada, hangi JSON path'te neyin yanlış olduğunu söylüyor, uygulama log'unu okumayı gerektirmiyor.
Üretimde Dikkat
Şema bir sözleşme; uygulama kodu değiştiğinde güncellenmezse yanlış alarm üretir ya da hataları kaçırır. Bookstore'da kural, yeni bir options property'si ekleyen pull request'in şemayı da güncellemesi. Bunu unutmamanın yolu, şemayı bir unit test'te options class'ları ile karşılaştırmak ya da şemayı koddan üretmek; .NET 9 ile gelen JsonSchemaExporter bir tipten JSON Schema üretebiliyor. Üretilen şemaya pattern ve enum gibi kuralları elle eklemen gerekeceğini de hesaba kat.
additionalProperties: false'u bir framework'ün kendi bölümlerine (Logging, Kestrel) uygulama; onların şeması SchemaStore'da ve sürümle değişiyor. Kendi bölümlerinde ise kapalı tut. Uzak bir şema URL'si kullanıyorsan (SchemaStore gibi) CI'ın ağ erişimi olduğundan ve o şemanın sürümünün değişebileceğinden haberdar ol; kritik şemaları repo'ya kopyalamak sonucu sabitler.
Sık Düşülen Tuzaklar
En sık olanı additionalProperties'i hiç yazmamak: varsayılan olarak her ek key serbest, yani şema yazım hatalarını hiç yakalamaz. İkincisi allOf ile birleştirilen şemalarda additionalProperties: false kullanmak: bu keyword yalnızca kendi bulunduğu şemadaki properties'i görür, diğer parçadan gelen alanları "fazla" sayar. Bu durum için 2020-12'deki unevaluatedProperties var.
Üçüncüsü draft karışıklığı: bir draft-07 şemasında $defs, prefixItems veya unevaluatedProperties kullanmak ve bunların sessizce yok sayılması. Dördüncüsü format'ın her doğrulayıcıda kontrol edildiğini sanmak. Son olarak JSONC dosyalarını düz JSON parser'ıyla doğrulamaya çalışmak: appsettings.json'daki ilk yorumda doğrulayıcı dosyayı parse edemez ve şemaya hiç bakmadan hata verir.
Küçük Bir Deneme
★☆☆ Bookstore.Api'nin appsettings.json'ına Shipping bölümünü ekle ve bilerek "Carriers": ["standard", "standard"] yaz. Hangi keyword bunu yakalıyor?
★★☆ Şemaya Ordering için bir kural ekle: InstitutionalDiscountRate verildiyse InstitutionalMinQuantity da verilmeli. 2020-12'deki dependentRequired keyword'ünü kullan ve aynı kuralı draft-07 $schema'sıyla dene.
★★★ JsonSchemaExporter ile CatalogOptions class'ından bir şema üret ve elle yazdığımız şemanın catalog bölümüyle karşılaştır. Hangi kurallar koddan çıkarılabildi, hangileri çıkarılamadı?
Şema ile dosyaları yazarken ve CI'da doğrulamayı gördük. Serinin son yazısında komut satırına geçiyoruz: jq ile JSON'u, yq ile YAML'ı sorgulamak, dönüştürmek ve yerinde düzenlemek; Bookstore'un manifest'inde image tag'ini güncellemekten bir GitHub Actions adımında jq ile koşul yazmaya kadar.