Swagger Inheritance and Composition

82

W moim „uproszczonym” interfejsie API wszystkie odpowiedzi są wyprowadzane ( dziedziczone ) z podstawowej klasy „odpowiedzi”. Klasa odpowiedzi składa się z nagłówka wypełnionego metadanymi oraz treści zawierającej podstawowe dane, o które prosi użytkownik. Odpowiedź (w formacie JSON) jest ułożona w taki sposób, że wszystkie metadane znajdują się na pierwszej „warstwie”, a treść jest pojedynczym atrybutem o nazwie „body”.

response
|--metadata attribute 1 (string/int/object)
|--metadata attribute 2 (string/int/object)
|--body (object)
    |--body attribute 1 (string/int/object)
    |--body attribute 2 (string/int/object)

Próbowałem zdefiniować tę relację z zachwytem za pomocą następującego kodu JSON:

{
    ...
    "definitions": {
        "response": {
            "allOf": [
                {
                    "$ref": "#/definitions/response_header"
                },
                {
                    "properties": {
                        "body": {
                            "description": "The body of the response (not metadata)",
                            "schema": {
                                "$ref": "#/definitions/response_body"
                            }
                        }
                    }
                }
            ]
        },
        "response_header": {
            "type": "object",
            "required": [
                "result"
            ],
            "properties": {
                "result": {
                    "type": "string",
                    "description": "value of 'success', for a successful response, or 'error' if there is an error",
                    "enum": [
                        "error",
                        "success"
                    ]
                },
                "message": {
                    "type": "string",
                    "description": "A suitable error message if something went wrong."
                }
            }
        },
        "response_body": {
            "type": "object"
        }
    }
}

Następnie próbuję utworzyć różne odpowiedzi, tworząc różne klasy treści / nagłówka, które dziedziczą po treści / nagłówku, a następnie tworzę klasy odpowiedzi potomnych, które składają się z odpowiednich klas nagłówka / treści (pokazane w kodzie źródłowym na dole). Jestem jednak pewien, że albo jest to niewłaściwy sposób robienia rzeczy, albo że moja implementacja jest nieprawidłowa. Nie udało mi się znaleźć przykładu dziedziczenia w specyfikacji Swagger 2.0 (pokazanej poniżej), ale znalazłem przykład kompozycji .

wprowadź opis obrazu tutaj

Jestem prawie pewien, że ten „dyskryminator” ma dużą rolę do odegrania, ale nie wiem, co powinienem zrobić.

Pytanie

Czy ktoś mógłby mi pokazać, jak należy zaimplementować kompozycję + dziedziczenie w swagger 2.0 (JSON), najlepiej "naprawiając" mój przykładowy kod poniżej. Byłoby również świetnie, gdybym mógł określić klasę ErrorResponse, która dziedziczy z odpowiedzi, w której atrybut „result” w nagłówku jest zawsze ustawiony na „error”.

{
    "swagger": "2.0",
    "info": {
        "title": "Test API",
        "description": "Request data from the system.",
        "version": "1.0.0"
    },
    "host": "xxx.xxx.com",
    "schemes": [
        "https"
    ],
    "basePath": "/",
    "produces": [
        "application/json"
    ],
    "paths": {
        "/request_filename": {
            "post": {
                "summary": "Request Filename",
                "description": "Generates an appropriate filename for a given data request.",
                "responses": {
                    "200": {
                        "description": "A JSON response with the generated filename",
                        "schema": {
                            "$ref": "#/definitions/filename_response"
                        }
                    }
                }
            }
        }
    },
    "definitions": {
        "response": {
            "allOf": [
                {
                    "$ref": "#/definitions/response_header"
                },
                {
                    "properties": {
                        "body": {
                            "description": "The body of the response (not metadata)",
                            "schema": {
                                "$ref": "#/definitions/response_body"
                            }
                        }
                    }
                }
            ]
        },
        "response_header": {
            "type": "object",
            "required": [
                "result"
            ],
            "properties": {
                "result": {
                    "type": "string",
                    "description": "value of 'success', for a successful response, or 'error' if there is an error",
                    "enum": [
                        "error",
                        "success"
                    ]
                },
                "message": {
                    "type": "string",
                    "description": "A suitable error message if something went wrong."
                }
            }
        },
        "response_body": {
            "type": "object"
        },
        "filename_response": {
            "extends": "response",
            "allOf": [
                {
                    "$ref": "#definitions/response_header"
                },
                {
                    "properties": {
                        "body": {
                            "schema": {
                                "$ref": "#definitions/filename_response_body"
                            }
                        }
                    }
                }
            ]
        },
        "filename_response_body": {
            "extends": "#/definitions/response_body",
            "properties": {
                "filename": {
                    "type": "string",
                    "description": "The automatically generated filename"
                }
            }
        }
    }
}

Aktualizacja diagramu

Aby spróbować wyjaśnić, czego chcę, utworzyłem poniżej bardzo podstawowy diagram, który ma na celu pokazanie, że wszystkie odpowiedzi są instancjami obiektu „response”, który został zbudowany przez (kompozycja) przy użyciu dowolnej kombinacji obiektów response_header i response_body. Obiekty response_header i response_body mogą być rozszerzone i wstawione do dowolnego obiektu odpowiedzi, co ma miejsce w przypadku pliku nazwa_odpowiedzi, która używa elementu potomnego filename_response_body z podstawowej klasy response_body. Zarówno błędy, jak i pomyślne odpowiedzi używają obiektu „odpowiedź”.

wprowadź opis obrazu tutaj

Programista
źródło
1
Nie jest próbka do składu, ale to jest tak źle to nie warto się dzielić. Będę pracował nad tym, jak powinna wyglądać Twoja specyfikacja. Należy pamiętać, że interfejs użytkownika obecnie go nie obsługuje, ale będzie to możliwe, gdy będzie dostępna pełna obsługa wersji 2.0.
Ron
1
A zanim się zanurzę, jeszcze jedno - szukasz kompozycji czy spadku? Kompozycja w zasadzie mówi I have the properties of X and my own properties.. Dziedziczenie sugeruje związek X is my parent. I have its properties and my own.. Dziedziczenie jest przydatne, jeśli chcesz powiedzieć, że używany jest określony zestaw modeli, w których rodzic jest używany.
Ron,
1
Miałem raczej nadzieję, że w tym przykładzie za jednym razem pokażę użycie dziedziczenia i kompozycji. Oczywiście zdaję sobie sprawę, że z łatwością można by użyć któregoś z nich samodzielnie, ale w tym przypadku wszystkie odpowiedzi są dziećmi podstawowej klasy „odpowiedzi”. Klasa odpowiedzi „składa się” z dwóch innych obiektów, nagłówka i treści.
Programster
2
Mogłem nie wyrazić się jasno. Dziedziczenie to rozszerzenie kompozycji. Jeśli istnieje dziedzictwo, istnieje kompozycja. Jeśli istnieje kompozycja, niekoniecznie jest to dziedziczenie. W twojej próbce model „odpowiedzi” nie jest nigdzie używany. Powinienem to zignorować i po prostu pokazać, jak powinno wyglądać?
Ron
ach, nie zdawałem sobie sprawy z tego związku między spadkiem a składem. Więc użyj dziedziczenia, aby pokazać oba. Jeśli chodzi o model odpowiedzi, który nie jest używany, należy go użyć z rozszerzeniem w elemencie potomnym nazwa_pliku_response, z którym odpowiada żądanie.
Programster

Odpowiedzi:

114

Jako początkujący zuchwale nie uważam oficjalnej dokumentacji dotyczącej polimorfizmu i kompozycji za łatwą do zrozumienia, bo brakuje w niej przykładu . Kiedy przeszukiwałem sieć, jest wiele dobrych przykładów odnoszących się do Swagger 1.2, kiedy extendsbył ważny.

Dla swaggera 2.0 znalazłem dobry przykład w źródłach specyfikacji na github za pośrednictwem tej grupy google

Na podstawie powyższych źródeł, oto krótki prawidłowy przykład dziedziczenia w YAML:

definitions:
  Pet:
    discriminator: petType
    required:
      - name
      - petType # required for inheritance to work
    properties:
      name: 
        type: string
      petType:
        type: string
  Cat:
    allOf:
      - $ref: '#/definitions/Pet' # Cat has all properties of a Pet
      - properties: # extra properties only for cats
          huntingSkill:
            type: string
            default: lazy
            enum:
              - lazy
              - aggressive
  Dog:
    allOf:
      - $ref: '#/definitions/Pet' # Dog has all properties of a Pet
      - properties: # extra properties only for dogs
          packSize:
            description: The size of the pack the dog is from
            type: integer
Tomasz Sętkowski
źródło
Naprawdę dzięki! To działa dla mnie. W programie editor.swagger.iowidzę mały błąd: w sekcji modeli widzę Petmodel wiele razy. Zawartość tych modeli jest w porządku. Tylko nazwy są błędne.
schellingerht
@schellingerht W editor2.swagger.ionie zobaczysz tego problemu
Shiplu Mokaddim
Jedynym problemem, jaki znalazłem w tym sposobie definiowania dziedziczenia, jest to, że właściwość petType jest nieco bezużyteczna w wygenerowanej klasie. Będzie pusty. Ale przynajmniej generuje hierarchię klas, tak jak myślałem. Dzięki!
xarlymg89
Aby utworzyć dziedziczenie json jak powyżej, musisz dodać adnotacje do swoich klas nadrzędnych i podrzędnych w następujący sposób: @ApiModel (dyskryminator = "type", subTypes = {Cat.class, Dog.class}) publiczna abstrakcyjna klasa Animal {} @ ApiModel (parent = Animal.class) public calss Cat extends Animal {}
Janet
Czy dyskryminator jest używany tylko wtedy, gdy implementujemy interfejs Pet, a jeśli klasa A rozszerza klasę B, czy też powinniśmy go używać? Dziękuję
Bionix1441
23

Odkryłem, że kompozycja działa dobrze nawet bez definicji discriminator.

Na przykład podstawa Response:

definitions:
  Response:
    description: Default API response
    properties:
      status:
        description: Response status `success` or `error`
        type: string
        enum: ["success", "error"]
      error_details:
        description: Exception message if called
        type: ["string", "object", "null"]
      error_message:
        description: Human readable error message
        type: ["string", "null"]
      result:
        description: Result body
        type: ["object", "null"]
      timestamp:
        description: UTC timestamp in ISO 8601 format
        type: string
    required:
      - status
      - timestamp
      - error_details
      - error_message
      - result

Jest renderowany jako:

Wizualizacja odpowiedzi

I możemy go rozszerzyć, aby udoskonalić niestandardowy schemat resultpola:

  FooServiceResponse:
    description: Response for Foo service
    allOf:
      - $ref: '#/definitions/Response'
      - properties:
          result:
            type: object
            properties:
              foo_field:
                type: integer
                format: int32
              bar_field:
                type: string
        required:
          - result

I zostanie poprawnie wyrenderowany jako:

Wizualizacja FooServiceResponse

Zauważ, że allOfto wystarczy, aby to zadziałało i żadne discriminatorpole nie jest używane. To dobrze, bo działa i to jest ważne, bo myślę, że narzędzia będą w stanie wygenerować kod bez discriminatorpól.

oblalex
źródło
Użyłem również allOf, ale w jakiś sposób openapi.yamlstwierdzam, że podklasy zawierają właściwości superklasy w sposób nadmiarowy, czy to prawda?
Bionix1441
9

Wszystkie odpowiedzi tutaj są już doskonałe, ale chcę tylko dodać drobną uwagę na temat kompozycji w porównaniu z dziedziczeniem . Zgodnie ze specyfikacją Swagger / OpenAPI , aby zaimplementować kompozycję , allOfwystarczy użyć właściwości, jak słusznie wskazuje @oblalex . Jednak aby zaimplementować dziedziczenie , trzeba skorzystać allOfz discriminator, jak w przykładzie @ TomaszSętkowski .

Znalazłem także więcej przykładów Swagger zarówno kompozycji, jak i dziedziczenia w API Handyman. Są częścią doskonałej serii tutoriali Swagger / OpenAPI autorstwa Arnauda Laureta, którą myślę, że każdy powinien sprawdzić.

DynamicDispatch
źródło
1
@ Chociaż zamieszczanie odpowiednich linków to dobry początek, aby faktycznie być użyteczną odpowiedzią, należy również zacytować odpowiedni tekst, który będzie znajdował się pod linkiem. Odradza się udzielanie odpowiedzi zawierających tylko linki, ponieważ linki często są martwe.
Stijn de Witt,
3

Standardowy przykład Swagger 2.0, który udostępniłeś, przedstawia relację kompozycji, a konkretnie przedstawia „to rodzaj” relacji nadtyp / podtyp, ale nie jest to polimorfizm sam w sobie.

Byłoby tak, gdybyś mógł odwołać się do podstawowej definicji Pet jako parametru wejściowego, a następnie wybrać Cat lub wprowadzić obiekt Cat JSON jako wartość dla żądania wejściowego i mieć to akceptowalne dla interfejsu Swagger UI.

Nie mogłem zmusić tego do bezpośredniego działania.

Najlepsze, co mogłem uzyskać, to ustawić dodatkowe właściwości na true w obiekcie podstawowym (np. Pet), określić Pet za pomocą odniesienia wskaźnika JSON jako schematu wejściowego, a na koniec skopiować i wkleić mój obiekt wartości Cat JSON do interfejsu użytkownika Swagger. Ponieważ dodatkowe właściwości są dozwolone, interfejs użytkownika Swagger wygenerował prawidłowy ładunek żądania wejściowego.

user6317389
źródło
Nie robisz polimorfizmu przez sieć (ani nie ujawniasz swoich jednostek danych) ... chyba że chcesz ściśle powiązać się z konkretnym hackiem, aby to zadziałało.
user1496062
Polimorfizm jest włączany przez dziedziczenie, ale nie jest wymagany do korzystania z dziedziczenia. Logicznie rzecz biorąc, dziedziczenie jest relacją typu „jest-jest”, podczas gdy kompozycja jest relacją „ma-a”. Granica między nimi może być niewyraźna w zależności od języka implementacji i przypadków użycia domeny. Ale to jest punkt wyjścia. Fwiw, dyskryminator umożliwia deserializację typów polimorficznych. Istnieją inne podejścia (np. W tym nazwy klas Java). Ale zgodziliśmy się, że mogą być niezdarne i nie przenośne. Np. Co klient Pythona zrobi z nazwami klas Java?
Charlie Reitzel