Elle yazılan bir OpenAPI 3.1 YAML dosyası beş ana parçadan oluşur: şartname sürümünü söyleyen openapi, API'nin adını ve sürümünü taşıyan info, sunucu adreslerini veren servers, uç noktaları ve işlemleri tarif eden paths ve tekrar kullanılan şemaları, parametreleri, yanıtları ve güvenlik tanımlarını toplayan components. Tekrarları $ref ile components altındaki tanımlara bağlarsınız. Dosyayı yazdıktan sonra bir doğrulayıcıdan geçirmek, aracın belgeyi okuyamayacağı hataları dakikalar içinde gösterir.
Bu yazı en küçük geçerli belgeden başlayıp kullanıcı listeleme, oluşturma ve getirme işlemlerini, sayfalama parametrelerini, Bearer kimlik doğrulamasını ve Problem Details hata yanıtlarını içeren eksiksiz bir belgeye ilerliyor. Her alanın anlamı OpenAPI Şartnamesi 3.1.1 metnine göre açıklanıyor; belgenin tamamı ve kasıtlı bozulmuş hâlleri openapi-spec-validator ile denetlendi. OpenAPI'nin ne olduğu ve FastAPI'nin belgeyi koddan nasıl ürettiği OpenAPI ve Swagger yazısında anlatılıyor.
En küçük geçerli belge

Şartnameye göre kök nesnede yalnızca iki alan zorunludur: openapi ve info. info içinde de title ve version zorunludur. Bunlara ek olarak bir tarif paths, components ya da webhooks alanlarından en az birini içermelidir. Aşağıdaki dosya bu yüzden geçerlidir; içinde hiç yol olmasa da boş bir paths nesnesi bulunur.
openapi: 3.1.1
info:
title: Kullanıcı API
version: 1.0.0
paths: {}Aynı dosyadan paths: {} satırını sildiğinizde doğrulayıcı belgeyi reddeder. Denetim için Python'da tek satırlık bir komut yeterli; aşağıdaki çıktılar bu iki dosyayla alındı (hata mesajındaki belge içeriği kısaltıldı):
pip install openapi-spec-validator pyyaml
python -c "import yaml,sys; from openapi_spec_validator import validate; validate(yaml.safe_load(open(sys.argv[1],encoding='utf-8')))" minimal.yaml && echo geçerli
# geçerli
python -c "import yaml,sys; from openapi_spec_validator import validate; validate(yaml.safe_load(open(sys.argv[1],encoding='utf-8')))" enaz_bos.yaml
# OpenAPIValidationError: {... } is not valid under any of the given schemasopenapi alanına yazılan sürüm şartnamenin sürümüdür ve araçlar belgeyi buna göre yorumlar. info.version ise sizin belgenizin sürümüdür; API'nize yeni bir uç nokta eklediğinizde bunu artırırsınız, openapi değerini değil. Şartname yama sürümleri arasında ayrım yapılmamasını söylediği için 3.1.0 ya da 3.1.1 yazmanız araçlar açısından aynı anlama gelir.
YAML yazarken bilinmesi gereken kurallar
OpenAPI belgesi aslında bir JSON nesnesidir; YAML yalnızca onu yazmanın daha okunaklı bir yoludur. Şartname, YAML ile JSON arasında kayıpsız dönüşüm yapılabilmesi için YAML 1.2'yi önerir ve iki kısıt koyar: etiketler YAML'ın JSON şema kurallarıyla sınırlı kalmalı, eşlem anahtarları da düz dize olmalıdır. Bu kısıtın pratikteki en önemli sonucu durum kodlarıdır. Şartname Responses nesnesindeki durum kodu anahtarlarının JSON ve YAML uyumu için tırnak içinde yazılması gerektiğini söyler: '200'. Tırnaksız 200 YAML ayrıştırıcısında tamsayıya dönüşür ve aşağıda göreceğiniz gibi doğrulayıcıyı çökertir.
İkinci kural büyük-küçük harftir: şartnamedeki bütün alan adları harf duyarlıdır. operationId yerine operationID yazmak alanı tanınmaz hâle getirir. Aynı kural her değer için geçerli değildir; şartname, HTTP kavramlarına doğrudan karşılık gelen ad ve değerlerde HTTP'nin kendi duyarlılık kurallarının geçerli olduğunu belirtir. Örneğin başlık adları HTTP'de harf duyarsız olduğu için Location ile location aynı başlığı anlatır.
Eksiksiz örnek belge

Aşağıdaki belge küçük bir kullanıcı API'sini tarif ediyor: kullanıcıları sayfalı listeleme ve rol ile filtreleme, yeni kullanıcı oluşturma ve kimliğe göre getirme. Belgenin tamamı doğrulayıcıdan hatasız geçti. Uzun görünebilir, fakat parçaların çoğu components altında bir kez tanımlanıp başka yerlerden $ref ile çağrılıyor. Alan adları Türkçe seçildi; /users gibi İngilizce adlar da aynı derecede yaygındır.
openapi: 3.1.1
info:
title: Kullanıcı API
summary: Kullanıcı kaynaklarını yöneten örnek REST API
version: 1.0.0
servers:
- url: https://api.ornek.com.tr/v1
paths:
/kullanicilar:
get:
operationId: kullanicilariListele
summary: Kullanıcıları listele
tags: [kullanicilar]
parameters:
- $ref: '#/components/parameters/Sayfa'
- $ref: '#/components/parameters/Limit'
- name: rol
in: query
schema:
type: string
enum: [admin, editor, okur]
responses:
'200':
description: Kullanıcı listesi
content:
application/json:
schema:
$ref: '#/components/schemas/KullaniciListesi'
'401':
$ref: '#/components/responses/Yetkisiz'
post:
operationId: kullaniciOlustur
summary: Yeni kullanıcı oluştur
tags: [kullanicilar]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/KullaniciOlustur'
example:
email: zeynep@ornek.com.tr
ad: Zeynep Kaya
rol: editor
responses:
'201':
description: Kullanıcı oluşturuldu
headers:
Location:
description: Yeni kaynağın adresi
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Kullanici'
'422':
$ref: '#/components/responses/DogrulamaHatasi'
/kullanicilar/{kullanici_id}:
parameters:
- name: kullanici_id
in: path
required: true
schema:
type: integer
format: int64
get:
operationId: kullaniciGetir
summary: Kimliğe göre kullanıcı getir
tags: [kullanicilar]
responses:
'200':
description: Kullanıcı bulundu
content:
application/json:
schema:
$ref: '#/components/schemas/Kullanici'
'404':
$ref: '#/components/responses/Bulunamadi'
security:
- bearerAuth: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
Sayfa:
name: page
in: query
schema:
type: integer
minimum: 1
default: 1
Limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
schemas:
KullaniciOlustur:
type: object
required: [email, ad, rol]
properties:
email:
type: string
format: email
ad:
type: string
minLength: 1
rol:
type: string
enum: [admin, editor, okur]
Kullanici:
allOf:
- $ref: '#/components/schemas/KullaniciOlustur'
- type: object
required: [id, created_at]
properties:
id:
type: integer
format: int64
created_at:
type: string
format: date-time
telefon:
type: [string, 'null']
KullaniciListesi:
type: object
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Kullanici'
meta:
type: object
properties:
total:
type: integer
page:
type: integer
limit:
type: integer
pages:
type: integer
Problem:
type: object
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
responses:
Yetkisiz:
description: Kimlik bilgisi yok ya da geçersiz
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
Bulunamadi:
description: Kaynak bulunamadı
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
DogrulamaHatasi:
description: Gövde doğrulamadan geçmedi
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'Şimdi belgeyi bölüm bölüm inceleyelim.
servers ve paths
servers listesindeki url, yolların başına eklenen taban adrestir. Şartnameye göre yol, sunucu adresine göreli URL çözümlemesi yapılmadan doğrudan eklenir; yani https://api.ornek.com.tr/v1 ile /kullanicilar birleşince https://api.ornek.com.tr/v1/kullanicilar olur. Sürüm önekini sunucu adresine koymak, her yolda /v1 tekrarını önler (API sürümleme). servers hiç verilmezse varsayılan adres / kabul edilir.
paths altındaki her anahtar / ile başlamak zorundadır. Süslü parantezli parçalar yol şablonudur (path templating) ve her şablon ifadesinin karşılığında bir yol parametresi tanımlanmalıdır. Şartname iki eşleşme kuralı daha koyar: somut bir yol (/kullanicilar/ben) şablonlu karşılığından (/kullanicilar/{kullanici_id}) önce eşleşir; aynı hiyerarşide yalnızca parametre adı farklı olan iki şablonlu yol ise özdeş sayılır ve birlikte bulunamaz.
parameters
/kullanicilar/{kullanici_id}:
parameters:
- name: kullanici_id
in: path
required: true
schema:
type: integer
format: int64Parametre nesnesinde name ve in zorunludur; in değeri query, header, path ya da cookie olabilir. Yol parametrelerinde required alanı zorunludur ve değeri true olmalıdır; diğer konumlarda varsayılanı false'tur. Yukarıdaki parametre işlem düzeyinde değil yol düzeyinde tanımlandı; şartnameye göre yol düzeyindeki parametreler o yoldaki bütün işlemlere uygulanır, işlem düzeyinde geçersiz kılınabilir ama kaldırılamaz. Bir yola ileride put ya da delete eklediğinizde kimlik parametresini yeniden yazmanız gerekmez. Bir ayrıntı daha: in: header ile adı Accept, Content-Type ya da Authorization olan bir parametre tanımlarsanız şartname bu tanımın yok sayılacağını söyler; bu başlıklar sırasıyla içerik tanımlarıyla ve güvenlik şemalarıyla anlatılır.
parameters:
Sayfa:
name: page
in: query
schema:
type: integer
minimum: 1
default: 1
Limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20Sayfalama parametreleri birden çok liste uç noktasında aynı olacağı için components.parameters altında tanımlandı ve listeleme işleminde $ref ile çağrıldı. maximum: 100 değeri yalnızca belge değil, sunucunun da uygulaması gereken bir sözleşmedir; neden bir üst sınır gerektiği offset sayfalama yazısında anlatılıyor.
requestBody ve responses
requestBody içindeki content zorunludur ve anahtarı bir medya türüdür. Şartnameye göre required alanının varsayılanı false'tur; yani gövdesiz bir POST'u kabul etmeyecekseniz required: true yazmanız gerekir. Örnekte ayrıca Media Type nesnesinin example alanıyla bir örnek gövde verildi; belgeyi okuyan araçlar bunu kullanıcıya gösterebilir. Şartname bir örneğin şemaya uyması gerektiğini (SHOULD) söyler.
Her Response nesnesinde description zorunludur; en sık rastlanan doğrulama hatası budur. 201 yanıtında headers altında Location başlığı tanımlandı: yeni kaynağın adresini döndürmek CRUD tasarımında anlatılan bir gelenektir ve belgede de yer almalıdır. Şartname yanıt başlığı olarak Content-Type tanımlanırsa bunun yok sayılacağını belirtir, çünkü tür content anahtarından zaten bellidir. Durum kodu yerine 4XX gibi aralıklar ya da default da kullanılabilir; açık bir kod tanımı aynı koddaki aralık tanımına üstün gelir.
components ve $ref
responses:
Yetkisiz:
description: Kimlik bilgisi yok ya da geçersiz
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'components altındaki tanımlar kendi başlarına API'yi etkilemez; şartnamenin ifadesiyle, dışarıdan açıkça başvurulmadıkça hiçbir etkileri yoktur. Anahtar adları ^[a-zA-Z0-9\.\-_]+$ düzenli ifadesine uymalıdır; bu yüzden Türkçe karakter içeren Bulunamadı gibi bir ad geçersizdir, örnekte Bulunamadi yazılmasının nedeni budur. Başvuru $ref: '#/components/schemas/Problem' biçiminde bir URI'dir; # aynı belgeyi, ardından gelen kısım belgede izlenecek yolu gösterir. Başka bir dosyadaki tanıma da göreli bir adresle başvurabilirsiniz; şartname büyük tarifleri birden çok belgeye bölmeye izin verir.
Reference nesnesinin yanına yalnızca summary ve description yazılabilir; şartnameye göre bunlar dışındaki ek alanlar yok sayılır. Schema nesnesi içindeki $ref ise JSON Schema anahtar sözcüğüdür ve başka anahtarlarla yan yana kullanılabilir. Hata yanıtları application/problem+json türüyle ve RFC 9457'deki type, title, status, detail, instance alanlarıyla tanımlandı (Problem Details).
Schema nesnesi: allOf, null ve format
Kullanici:
allOf:
- $ref: '#/components/schemas/KullaniciOlustur'
- type: object
required: [id, created_at]
properties:
id:
type: integer
format: int64
created_at:
type: string
format: date-time
telefon:
type: [string, 'null']OpenAPI 3.1'in Schema nesnesi JSON Schema 2020-12'nin bir üst kümesidir. Bunun iki pratik sonucu var. Birincisi, null artık ayrı bir veri türüdür; değeri boş olabilecek bir alan type: [string, 'null'] diye yazılır. İkincisi, şemalar allOf ile birleştirilebilir: Kullanici, oluşturma gövdesindeki alanları yeniden yazmak yerine KullaniciOlustur şemasını alıp id ve created_at ekliyor. Şartname, allOf içindeki şemaların bağımsız doğrulandığını ve birlikte tek bir nesne oluşturduğunu, ama aralarında bir hiyerarşi kurmadığını belirtir.
format konusunda yanılgı yaygındır. Şartnameye göre format varsayılan olarak doğrulama yapmayan bir açıklamadır (annotation) ve doğrulama desteği araçtan araca değişir. format: email yazmak, her aracın e-posta biçimini denetleyeceği anlamına gelmez; sunucunuz bu denetimi kendisi yapmalıdır. Aynı bölüm, pattern ya da date-time gibi dizeye özgü kuralların başka türdeki değerleri otomatik olarak geçerli saydığını da hatırlatır: türü kısıtlamak istiyorsanız type anahtarını açıkça yazmalısınız. Tarih alanları için RFC 3339 biçimi ile format: date-time birlikte kullanılır.
security
security:
- bearerAuth: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWTGüvenlik şeması components.securitySchemes altında tanımlanır, kök düzeydeki security listesiyle bütün işlemlere uygulanır. type: http ve scheme: bearer, isteğin Authorization: Bearer ... başlığıyla kimlik doğrulayacağını söyler; bearerFormat ise şartnameye göre yalnızca belgeleme amaçlı bir ipucudur. Listede birden çok öğe varsa bunlardan yalnızca birinin sağlanması yeterlidir; tek bir öğe içinde birden çok şema varsa hepsi sağlanmalıdır. Bir işlemi kimlik doğrulamasız bırakmak için işlem düzeyinde security: [] yazılır. Bearer token'ın kendisi JWT Bearer kimlik doğrulama ve API key yazılarında.
Doğrulayıcıyla hata ayıklama
Aşağıdaki betik tam belgeyi yükleyip her seferinde tek bir yeri bozuyor ve doğrulayıcının tepkisini yazdırıyor. Gerçek belgelerde sık karşılaşılan hataların çoğu bu listede.
import copy
import yaml
from openapi_spec_validator import validate
asil = yaml.safe_load(open("tam.yaml", encoding="utf-8"))
def dene(ad, degistir):
d = copy.deepcopy(asil)
degistir(d)
try:
validate(d)
print(ad, "-> geçerli")
except Exception as e:
print(ad, "->", type(e).__name__ + ":", str(e).splitlines()[0].split(" within")[0])
dene("tam belge", lambda d: None)
dene("description yok", lambda d: d["paths"]["/kullanicilar/{kullanici_id}"]["get"]["responses"]["200"].pop("description"))
dene("required false", lambda d: d["paths"]["/kullanicilar/{kullanici_id}"]["parameters"][0].update(required=False))
dene("tırnaksız 200", lambda d: d["paths"]["/kullanicilar"]["get"]["responses"].__setitem__(200, d["paths"]["/kullanicilar"]["get"]["responses"].pop("200")))
dene("kırık $ref", lambda d: d["paths"]["/kullanicilar/{kullanici_id}"]["get"]["responses"]["200"]["content"]["application/json"]["schema"].update({"$ref": "#/components/schemas/Kullanicii"}))
dene("info.version yok", lambda d: d["info"].pop("version"))
dene("nullable: true", lambda d: d["components"]["schemas"]["Kullanici"]["allOf"][1]["properties"]["telefon"].update(type="string", nullable=True))
dene("yol / ile başlamıyor", lambda d: d["paths"].__setitem__("kullanicilar", d["paths"].pop("/kullanicilar")))
# YAML'de tırnaksız 200 anahtarı tamsayı olarak okunur
print(type(list(yaml.safe_load("responses:\n 200:\n description: x\n")["responses"])[0]).__name__)
# çıktı:
# tam belge -> geçerli
# description yok -> OpenAPIValidationError: 'description' is a required property
# required false -> OpenAPIValidationError: True was expected
# tırnaksız 200 -> TypeError: expected string or bytes-like object, got 'int'
# kırık $ref -> PointerToNowhere: '/components/schemas/Kullanicii' does not exist
# info.version yok -> OpenAPIValidationError: 'version' is a required property
# nullable: true -> geçerli
# yol / ile başlamıyor -> OpenAPIValidationError: Unevaluated properties are not allowed ('kullanicilar' was unexpected)
# intSonuçları tek tek okuyalım. Yanıt açıklaması eksik olduğunda mesaj doğrudan eksik alanı söylüyor. Yol parametresinde required: false yazıldığında mesaj "True was expected" oluyor; şartnamedeki "değeri true olmalıdır" kuralı. Tırnaksız 200 anahtarı doğrulayıcıyı bir TypeError ile çökertti; mesaj sorunu açıkça söylemediği için bu hatayı tanımak önemlidir. Yanlış yazılmış bir şema adı PointerToNowhere hatası verdi. info.version eksikliği ve / ile başlamayan yol da yakalandı.
En öğretici satır nullable: true satırı: belge geçerli çıktı. nullable OpenAPI 3.0'da kullanılan bir anahtardı; 3.1 şartnamesinin Schema nesnesi tanımında böyle bir alan yoktur. Doğrulayıcının buna ses çıkarmamasının nedeni, şartnamenin Schema nesnesinde başka sözlüklerden ya da tamamen serbest anahtarların kullanılmasına izin vermesidir. Sonuç: belge geçerlidir, ama nullable 3.1 araçları için anlam taşımaz ve alan hâlâ yalnızca dize kabul eder. Doğrulayıcıdan geçmek, belgenin niyet ettiğiniz şeyi söylediğini garanti etmez.
Sık yapılan hatalar
- Durum kodunu tırnaksız yazmak: YAML anahtarı tamsayıya çevirir; şartname tırnak ister.
- Yanıtta description unutmak: Her Response nesnesinde zorunludur.
- 3.0 alışkanlıkları:
nullableyerinetypedizisi kullanın;exampleSchema nesnesinde kullanımdan kalkmıştır, JSON Schema'nınexamplesanahtarı önerilir. - Components adında Türkçe karakter: Anahtarlar yalnızca harf, rakam, nokta, tire ve alt çizgi içerebilir.
- format'a güvenmek: Doğrulama yapmayan bir açıklamadır; sunucu denetimini yerine koymaz.
- Parametreyi her işlemde tekrar yazmak: Yol düzeyinde ya da
components.parametersaltında bir kez tanımlayın. - Accept veya Authorization'ı header parametresi yapmak: Şartnameye göre bu tanımlar yok sayılır.
- operationId tekrarı: Bütün API'de benzersiz olmalıdır; kopyala-yapıştır sonrası en sık atlanan noktadır.
Sık Sorulan Sorular
YAML mı yazmalıyım, JSON mu?
İkisi de geçerlidir ve şartname aynı belgeyi tarif eder. YAML elle yazmak ve okumak için daha rahattır; JSON ise programların ürettiği belgelerde yaygındır. Kuralları uyguladığınız sürece biri diğerine kayıpsız çevrilebilir.
OpenAPI 3.0 belgemi 3.1 olarak işaretlemek yeterli mi?
Hayır. Şartname küçük sürümlerde bile geriye uyumsuz değişiklikler yapılabileceğini belirtir. nullable ve example gibi alanları gözden geçirmeniz gerekir; yukarıdaki örnekte gördüğünüz gibi doğrulayıcı her farkı yakalamaz.
Belge çok uzadı, nasıl bölebilirim?
Şemaları ayrı dosyalara koyup $ref: './schemas/kullanici.yaml' gibi göreli adreslerle başvurabilirsiniz. Şartname birden çok belgeden oluşan tarifleri destekler; ayrıştırmanın başladığı belgeye giriş belgesi denir.
Belgeyi yazdım, sunucuyla uyumlu olduğunu nasıl anlarım?
Doğrulayıcı yalnızca belgeyi denetler. Sunucunun belgeye uyduğunu görmek için gerçek yanıtları belgedeki şemalarla karşılaştıran testler yazmanız gerekir. FastAPI kullanıyorsanız çatının ürettiği belgeyi elle yazdığınız belgeyle karşılaştırmak hızlı bir ilk adımdır (FastAPI ile OpenAPI).
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: REST API tasarımı öğrenme rehberi · OpenAPI ve Swagger · webhook tasarımı · Problem Details · offset sayfalama · tasarım kontrol listesi