Kihagyás

OpenAPI 3 és a Swagger

A REST API-ról tudjuk, hogy nem szabvány, hanem "architekturális stílus". Azaz nem szigorú szabályokat követ, hanem irányelveket és szokásokat. Felmerül a kérdés, hogy amennyiben egy REST API-t integrációs eszköznek kívánunk használni, hogyan tudjuk minél szabványosabb módon dokumentálni? Web Service-ek esetén a SOAP szabvány, WSDL, DTD és XSD leírók valók erre a célra. Egy REST API-nál pedig az OpenAPI.

OpenAPI 3.0

Az OpenAPI tehát egy API leíró, amely egy REST API-t publikáló webalkalmazásban egy fájlként érhető el a webszerverről. Ez a specifikáció tartalmazza az API műveleteit, adattípusait, dokumentációját. Nézzük a tartalmát.

OpenAPI specifikációt a legritkább esetben írunk kézzel, helyette generáljuk a REST API-t megvalósító rendszerben technológiaspecifikus eszközökkel. Az alábbiakban a példák tehát csak szemléltetik, hogyan néz ki egy OpenAPI specifikáció és milyen koncepciókkal dolgozhatunk.

Meta információk

A metainformációk szakaszban adhatunk meg információkat az API-ról általánosan. Ebben a szakaszban olyan információkat lehet megadni, mint például, hogy mit csinál az API, mi az API alap URL címe, milyen webes protokollt követ, és milyen autentikációs megoldásokat támogat.

openapi: 3.0.0
info:
  version: 1.0.0
  title: Simple Artist API
  description: A simple API to illustrate OpenAPI concepts

servers:
  - url: https://example.io/v1

# Basic authentication
components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
security:
  - BasicAuth: []

paths: {}
...

API végpontok

Az általunk készített API végpontjait adjuk meg a paths alatt. Az alap URL-hez képes relatív elérési úton érhetjük el őket, jelen esetben /artist útvonalon. Specifikálhatunk bennük HTTP igéket, melyeket a végpontok használnak, ilyen például a get.

...
paths:
  /artists:
    get:
     description: Returns a list of artists 
...

Ha ASP.NET controllert képzelünk a végpont mögé, akkor a controller metódus valahogy így néz ki:

[HttpGet("artists")]
public IEnumerable<Artist> GetAll(){
    return artistRepository.List();
}

A végpontokban definiálnunk kell a válaszokat is, amiket a kliens kaphat. Ezeket a HTTP igék leírása alatt a responses tulajdonsághoz kell írnunk. Ehhez szükségünk van egy HTTP státusz kódra, illetve egy leírásra, amely a válasz sémáját ismerteti.

paths:
  /artists:
    get:
      responses:
        '200':
          description: Successfully returned a list of artists
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - username
                  properties:
                    artist_name:
                      type: string
                    artist_genre:
                      type: string
                    albums_recorded:
                      type: integer
                    username:
                      type: string

        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:   
                  message:
                    type: string
...

A fenti 200-as státusz kód esetén visszaadott lista egy eleme C#-ban az alábbiak szerint nézhet ki.

class Artist
{
  [Required]
  [JsonPropertyName("username")]
  public string Username { get; set; }

  [JsonPropertyName("artist_name")]
  public string ArtistName { get; set; }

  [JsonPropertyName("artist_genre")]
  public string ArtistGenre { get; set; }

  [JsonPropertyName("albums_recorded")]
  public int AlbumsRecorded { get; set; }
}

Paraméterek

Lehetőségünk van számtalan paraméter megadására, mellyel a kliensek és a szerver közötti kommunikáció specifikusságát tudjuk növelni. Ilyen paraméter például a lekérdezés paraméter (query parameter). Ahhoz, hogy egy például egy GET http://example.com/v1/artists?limit=20&offset=3 kérést ki tudjunk szolgálni a fenti kódot az alábbi sorokkal kell bővítenünk:

[HttpGet("artists")]
public IEnumerable<Artist> GetByLimOffs(int limit, int offset){
  return artistRepository.List(limit, offset);
}

Ennek az OpenAPI leírása pedig az alábbi lesz:

paths:
  /artists:
    get:
      parameters:
        - name: limit
          in: query
          description: Limits the number of items on a page
          schema:
            type: integer
        - name: offset
          in: query
          description: Specifies the page number of the artists to be displayed
...

Az query string mellett az URL path részében is szállítható paraméter, például a GET http://example.com/v1/artists/{username} formájában. Ekkor a C# kód így néz ki:

[HttpGet("artists/{username}")]
public IEnumerable<Artist> GetByUsername(string username){
    return artistRepository.GetByUname(username);
}

Ehhez pedig az alábbi OpenAPI specifikáció részlet tartozik.

paths:
  /artists/{username}:
    get:
      description: Obtain information about an artist from his or her unique username
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string

      responses:
        '200':
          description: Successfully returned an artist
          content:
            application/json:
              schema:
                type: object
                properties:
                  artist_name:
                    type: string
                  artist_genre:
                    type: string
                  albums_recorded:
                    type: integer

        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object 
                properties:           
                  message:
                    type: string

Példakódjaink egy alap API kezdetleges változatát állították elő. Jól látható, hogy ez a kód egy komolyabb API leírásnál hatalmas mennyiséget is elérhet. Fontos látni tehát, hogy az API leírásához előbb a logikailag összerakott adatbázist és egy azt reprezentáló modellt kell alkotnunk, mert csak az után képzelhető el az API modellje - amit ráadásul az előbbiekből fogunk tipikusan generálni.

Polimorfizmus és öröklés OpenAPI 3.0 alatt

Az öröklés és polimorfizmus is objektumorientált fogalmak. Az OpenAPI viszont nem objektumorientált - hiszen nem programozási nyelv. Mégis, az OpenAPI-ban is felmerül a kérdés, hogyan lehet ismétlődő adatrészeket "kiemelni", azaz, hogy a "közös" sémát ne kell sokszor ismételni. Objektumorientált nyelvben erre természetesen a leszármazás a megoldás. Az OpenAPI specifikációja is lehetőséget biztosít arra, hogy a közös tulajdonságok ne alkossanak sormintát, ezzel csúnyává téve az amúgy is hosszú OpenAPI definíciót. Nézzünk erre egy példát.

Tegyük fel, hogy két féle hibaüzenetet tudunk visszaadni, az egyik "részhalmaza" a másiknak. C#-ben mindez így nézne ki:

class BasicErrorModel
{
  public string Message { get; set; }
  public int Code { get; set; }
}
class ExtendedErrorModel : BasicErrorModel
{
  public string RootCause { get; set; }
}

Ez OpenApi-ban az allOf kulcsszó használatával írható le, és így néz ki:

    components:
      schemas:
        BasicErrorModel:
          type: object
          required:
            - message
            - code
          properties:
            message:
              type: string
            code:
              type: integer
              minimum: 100
              maximum: 600
        ExtendedErrorModel:
          allOf:     # "átveszi" a hivatkozott modell összes elemét és kiegészíti továbbiakkal
            - $ref: '#/components/schemas/BasicErrorModel'
            - type: object
              required:
                - rootCause
              properties:
                rootCause:
                  type: string

Az allOf segít abban, hogy egy relatív elérési út megadásával egyesítsük az általunk készített "leszármazott" attribútumait az elérési út túloldalán álló modell attribútumaival. Van egy szabály, amelyet ajánlott követnünk az allOf parancsnál, ez pedig nem más mint, hogy ne használjunk azonos attribútum neveket különböző adattípusokkal. Ha ilyen előfordul a kódban az számtalan hibához vezethet.

Másik, objektumorientált világban gyakran használt eszköz a polimorfizmus, azaz amikor a konkrét példány egy leszármazási hierarchia bármely típusa lehet. A polimorfizmus megoldásához az allOf-hoz hasonló oneOf kulcsszót tudjuk használni. Az alábbi példában a oneOf parancs lehetővé teszi, hogy az adat simpleObject vagy complexObject sémát is tartalmazhasson, de midig csak az egyiket.

...
    components:
      responses:
        sampleObjectResponse:
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/simpleObject'
                  - $ref: '#/components/schemas/complexObject'
     ...
    components:
      schemas:
        simpleObject:
          ...
        complexObject:
          ...

Amennyiben ezt C#-ban próbáljuk elképzelni, akkor SimpleObject és ComplexObject is osztályok, amelyek tipikusan (de nem szükségszerűen) rendelkeznek egy közös őssel.

Azon túl, hogy a polimorfizmust a fentiekben a sémában kifejeztük, valóban még nem vagyunk készen. Az OpenAPI ugyanis csak a sémáról szól. Arról nem, hogy a C#/Java/stb. kódban a tényleges adatból hogyan keletkezhet objektum. Hiszen ez nem az OpenAPI felelőssége - ez a sorosításra tartozik. A deszerializálás (JSON -> objektum) során kell majd valójában eldönteni, hogy az adat, ami érkezik, az melyik típusnak felel meg. Ezt a tipikus JSON sorosító könyvtárak csak külön konfiguráció árán tudják elvégezni. Ezzel tehát érdemes vizsgázni, az OpenAPI csak a probléma felét oldotta meg.

A szakirodalom véleménye ezért is megoszlik a polimorfizmusról mint lehetséges edge-case-ről. Az OpenAPI 3.0 kiadásával megjelentek a már fent említett parancsok, viszont még 2019-ben is számtalan olyan szolgáltatás volt, amely nem támogatta ezek használatát.

OpenAPI vs Swagger, mik is a különbségek?

Az OpenAPI és a Swagger a szakmában többnyire szinonimaként jelenik meg, de mást jelentenek hivatalosan. Az OpenAPI a "Swaggerből jött létre", annak szabványosított változata, míg a Swagger egy szoftvercsomag. 2017-ben az OpenAPI 3.0 megjelenése elég nagy mérföldkőnek számított. Ez volt az első hivatalos kiadás 2015 óta, amikor is a SmartBear Software az OpenAPI Initiative-nek ajándékozta a jogokat, illetve megtörtént a Swagger Specification -> OpenAPI Specification névváltás.

A különbség legegyszerűbben így érthető meg:

  • OpenAPI = specifikáció
  • Swagger = eszközök a specifikáció megvalósításához

Az OpenAPI a specifikáció hivatalos neve. A specifikáció fejlesztését az OpenAPI Initiative segíti, amelyben több mint 30 szervezet vesz részt az IT világ különböző területeiről - köztük a Microsoft, a Google, az IBM és a CapitalOne. A Swagger eszközök fejlesztését vezető Smartbear Software szintén tagja az OpenAPI Initiative-nek, és segíti a specifikáció fejlődését. A Swagger eszköztár nyílt forráskódú, ingyenes és kereskedelmi eszközök keverékét tartalmazza, amelyek az API életciklus különböző szakaszaiban használhatók:

  • Swagger Editor: lehetővé teszi az OpenAPI-specifikációk YAML-ben történő szerkesztését a böngészőben, valamint a dokumentációk valós idejű előnézetét.
  • Swagger UI: egy HTML, JavaScript és CSS eszközökből álló gyűjtemény, amely dinamikusan gyönyörű dokumentációt generál egy OAS-kompatibilis API-ból.
  • Swagger Codegen: Lehetővé teszi az API klienskönyvtárak (SDK generálása), szerver csonkok és dokumentáció automatikus generálását egy OpenAPI Spec alapján.
  • Swagger Parser: Önálló könyvtár az OpenAPI definíciók Java-ból történő elemzésére.
  • Swagger Core: Java-alapú könyvtárak az OpenAPI-definíciók létrehozásához, fogyasztásához és az azokkal való munkához.
  • Swagger Inspector (ingyenes): API-tesztelő eszköz, amely lehetővé teszi az API-k validálását és OpenAPI-definíciók generálását egy meglévő API-ból.
  • SwaggerHub (ingyenes és kereskedelmi): API-tervezés és dokumentáció, az OpenAPI-val dolgozó csapatok számára készült.

A Swagger számtalan lehetőséget nyújt a fejlesztők számára a leírások elkészítéséhez. Ezeket a leírásokat általában JSON vagy YAML nyelveken készítjük. A hivatalos források mind a YAML nyelvet ajánlják, ugyanis könnyebben olvasható és gyorsabban megérthető.

Források

A jegyzet elkészítéséhez a hivatalos dokumentáció szolgált forrásként:


2023-02-03 Szerzők