
TSOA Fallstricke: HTTP Methoden und Übergabeparameter (Deutsch)
Dies ist der dritte Teil einer Serie von Beiträgen zum Thema Fallstricke beim Einsatz von TSOA. Dabei geht es um Lösungen von Schwierigkeiten die im praktischen Einsatz dieses Frameworks auftreten. In diesem Beitrag geht es darum welche Parameter und Rückgabe Datentypen mit welchen HTTP Methoden (GET, PUT, POSt, DELETE) und TSOA kompatibel. Auch bei Verwendung von Feldern und optionalen Parametern sind einige Besonderheiten zu beachten.
HTTP Methoden und Übergabeparameter
Ein sehr umfangreiches Kapitel mit einigen Stolpersteinen ist das Thema HTTP Methoden (GET, PUT, POST, DELETE) in Kombination mit Übergabeparametern. Die Swagger OpenApi Spezifikation gibt bereits einige Regeln vor, welche Kombinationen vorgesehen sind. Version 2 und Version 3 definieren unterschiedliche Parameter Locations. Hier soll es vor allem um die folgenden (V2) Locations gehen.
- Query – Parameters that are appended to the URL. For example, in
/items?id=###
, the query parameter isid
. - Path – Used together with Path Templating, where the parameter value is actually part of the operation’s URL. This does not include the host or base path of the API. For example, in
/items/{itemId}
, the path parameter isitemId
. - Body – The payload that’s appended to the HTTP request. Since there can only be one payload, there can only be onebody parameter. The name of the body parameter has no effect on the parameter itself and is used for documentation purposes only. Since Form parameters are also in the payload, body and form parameters cannot exist together for the same operation.
Mögliche Kombinationen HTTP Methoden und Parameter Locations
Body | Path | Query | |
---|---|---|---|
GET | ⚠ Compilerfehler | ✅ | ✅ |
PUT | ✅ | ✅ | ✅ |
POST | ✅ | ✅ | ✅ |
DELETE | ⚠ Compilerfehler | ✅ | ✅ |
Beim Generieren mit TSOA kommt es bei der Kombination GET + Body und DELETE + Body zu folgenden Compilerfehlern. Diese Kombination wird nicht unterstützt.
[generate.swagger] Generate swagger error. [generate.swagger] Generate routes error. [generate.swagger] Error: @Body('value') Can't support in GET method.
[generate.swagger] Generate swagger error. [generate.swagger] Generate routes error. [generate.swagger] Error: @Body('value') Can't support in DELETE method.
Mehrere Übergabeparameter in Kombination
Die Swagger OpenApi Spezifikation erlaubt und TSOA unterstützt die Kombination unterschiedlicher Parameter Locations an einer Service Funktion.
An einer Service Funktion können mehrere Eingaben und Ausgaben definiert werden.
- mehrere Path Parameter möglich
- mehrere Query Parameter möglich
- ein Body Parameter möglich
- ein Result möglich
- ein Status Codes für erfolgreiche Ausführung möglich
- mehrere Status Codes für fehlerhafte Ausführung möglich
Die Reihenfolge der Parameter ist beliebig. Die Lesbarkeit profitiert aber von einer Path > Query > Body Reihenfolge. Zum Bespiel:
@SuccessResponse('204', 'successful') @Response('400', 'invalid input') @Response('404', 'not found') @Response('407', 'unknown error') @Put('{nr}') public async park (@Path() nr: number, @Query() days: number, @Query() start: Date, @Body() car: Car): Promise { // ... }
curl -X PUT "http://localhost:9091/v1/Garage/2?days=14&start=2018-05-22T08%3A32%3A46.371Z" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"plateNumber\": \"dd-ab 123\", \"manufacturer\": \"BMW\", \"model\": \"Z1\"}"
Pro Service Funktion ist nur ein Body Parameter zulässig
Während mehrere Eingabeparameter pro Servicefunktion per Path und Query übergeben werden können, darf pro Funktion nur ein Body Parameter angegeben werden. Folgendes Beispiel erzeugt beim Generieren der Service Schnittstelle mit TSOA einen Fehler.
@Put('store') public async setP3B (@Body() text?: string, @Body() zahl?: number, @Body() wahr?: boolean): Promise { // ... }
Generate swagger error. Error: Only one body parameter allowed in 'TestController.setP3B' method.
Parameter Locations und Datentypen
Die Service Schnittstellengenerierung von TSOA erlaubt nur bestimmte Kombinationen von Parameter Location und Datentyp.
Query |
Path |
Body |
|
---|---|---|---|
number |
✅ | ✅ | ⚠ |
boolean |
✅ | ✅ | ⚠ |
string |
✅ | ✅ | ⚠ |
Date |
✅ | ✅ | ⚠ |
[ ] Array |
✅ | ❌ | ✅ |
|
❌ | ❌ | ✅ |
Enum |
✅ | ✅ | ⚠ |
- da Query und Path Parameter in der URL übertragen werden, können alle primitiven Datentypen verwendet werden (
number, boolean, string
) ✅ - Date Werte werden von der generierten Schnittstelle wie Text / string behandelt, daher können sie als Query und Path Parameter als Teil der URL verwendet werden. ✅
- Enum Werte werden von der generierten Schnittstele wie Text / string behandelt, daher können sie als Query und Path Parameter als Teil der URL verwendet werden. ✅
- da Body Parameter als JSON Objekte interpretiert werden, können keine primitiven Datentypen und Enums als Body verwendet werden ⚠
- Arrays (z.B.
number[]
) können nicht als URL Pfad Element übertragen werden ❌ - Arrays können als Query oder Body JSON Objekt übertragen werden ✅
- JSON Objekte können nicht als Teil der URL übertragen werden ❌
- JSON Objekte können nur als Body Parameter übertragen werden ✅
Objekte als Query oder Path Parameter
Wird bei einer Service Funktion versehentlich ein (JSON) Objekt als Datentyp für einen Query oder Path Parameter angegeben, bricht TSOA die Generierung der Service Schnittstelle mit einem aussagkräftigen Fehler ab. ❌
[generate.swagger] Generate swagger error. [generate.swagger] Error: @Path('param') Can't support 'refObject' type. [generate.swagger] in 'CarController.functionWithPathObject'
[generate.swagger] Generate swagger error. [generate.swagger] Error: @Query('param') Can't support 'refObject' type. [generate.swagger] in 'CarController.functionWithQueryObject'
Array als Path Parameter
Wird bei einer Service Funktion versehentlich ein Array (z.B. number[]
) als Path Parameter angegeben, bricht TSOA die Generierung der Service Schnittstelle mit einem aussagekräftigen Fehler ab. ❌
[generate.routes] Generate routes error. [generate.routes] Error: @Path('param') Can't support 'array' type. [generate.routes] in 'CarController.functionWithPathArray
Enum als Body Parameter
Wird bei einer Service Funktion versehentlich ein Enum als Body Parameter angegeben, funktioniert die Generierung mit TSOA fehlerfrei. Beim Aufruf der Service Funktion mit einem Enum Wert wird der Aufruf mit einem Fehler abgebrochen. D.h. der Fehler tritt erst zur Laufzeit des generierten Dienstes auf. ❌
SyntaxError: Unexpected token # in JSON at position 0 at JSON.parse () at createStrictSyntaxError (/node_modules/body-parser/lib/types/json.js:157:10)
Primitive Übergabeparameter und Body
Die Verwendung von primitiven Datentypen (string, number, boolean, Date) als Body Parameter ist grundsätzlich möglich. TSOA generiert ohne Warnung eine passende Service Schnittstelle mit Swagger Beschreibung (swagger.json).
Die Verwendung von primitiven Übergabeparametern als Body Parameter kann zu Ausführungsfehlern des generierten Dienstes führen! ⚠
Im folgenden Beispiel wurden Setter mit verschiedenen Body Datentypen deklariert. Die TSOA Codegenerierung verläuft fehlerfrei und ohne Warnungen.
@Route('Car') export class CarController extends Controller { constructor () { super() } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/color') public async setColor (@Path('plateNr') plate: string, @Body() color: string): Promise<void> { // ... // no compile error, no warning } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/nextInspection') public async setNextInspection (@Path('plateNr') plate: string, @Body() date: Date): Promise<void> { // ... // no compile error, no warning } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/mileage') public async setMileage (@Path('plateNr') plate: string, @Body() miles: number): Promise<void> { // ... // no compile error, no warning } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/repair') public async setInRepair (@Path('plateNr') plate: string, @Body() repair: boolean): Promise<void> { // ... // no compile error, no warning } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/parts') public async setPartsIds (@Path('plateNr') plate: string, @Body() ids: number[]): Promise<void> { // ... // no compile error, no warning } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/weekdayCleaning') public async setCleaningDays (@Path('plateNr') plate: string, @Body() days: boolean[]): Promise<void> { // ... // no compile error, no warning } }
Über die Swagger Console kann eine HTML Beschreibung und Testoberfläche für die generierte Schnittstelle mit deren Funktionen angezeigt werden.
setColor, setNextInspection, setMileage, setInRepair
besitzen kein Eingabefeld für den Wert, die Funktionen lassen sich nicht ausführen ⚠setPartsIds, setCleaningDays
besitzen ein Eingabefeld für das Array und die Funktionen lassen sich ausführen ✅
Beim Aufrufen von setColor, setNextInspection, setMileage, setInRepair
über die URL wird ein Fehler zurückgegeben. ❌
curl -X PUT "http://localhost:9091/v1/Car/color" -H "accept: application/json" -H "Content-Type: application/json" -d "blue"
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>SyntaxError: Unexpected token # in JSON at position 0<br> at JSON.parse (<anonymous>)<br> at createStrictSyntaxError
[main] INFO io.swagger.parser.Swagger20Parser - reading from http://localhost:9091/swagger.json Exception in thread "main" java.lang.RuntimeException: Could not process operation: Tag: Tag { name: Car description: null externalDocs: null extensions:{}} Operation: SetColor Resource: put /Car/{plateNr}/color Definitions: {Car=io.swagger.models.ModelImpl@1c162f33, Garage=io.swagger.models.ModelImpl@c189fc22, IVersion=io.swagger.models.ModelImpl@431c44ff, UpdateImage=io.swagger.models.ModelImpl@2351e278, NetworkingMode=io.swagger.models.ModelImpl@f30acc6, NetworkConfig=io.swagger.models.ModelImpl@776cc6de, SysInfo=io.swagger.models.ModelImpl@b02fa48a} Exception: null at io.swagger.codegen.DefaultGenerator.processOperation(DefaultGenerator.java:916) at io.swagger.codegen.DefaultGenerator.processPaths(DefaultGenerator.java:793) at io.swagger.codegen.DefaultGenerator.generateApis(DefaultGenerator.java:418) at io.swagger.codegen.DefaultGenerator.generate(DefaultGenerator.java:730) at io.swagger.codegen.cmd.Generate.run(Generate.java:285) at io.swagger.codegen.SwaggerCodegen.main(SwaggerCodegen.java:35) Caused by: java.lang.NullPointerException at io.swagger.codegen.DefaultCodegen.isDataTypeBinary(DefaultCodegen.java:2734) at io.swagger.codegen.DefaultCodegen.fromParameter(DefaultCodegen.java:2685) at io.swagger.codegen.DefaultCodegen.fromOperation(DefaultCodegen.java:2241) at io.swagger.codegen.DefaultGenerator.processOperation(DefaultGenerator.java:864) ... 5 more
Wichtig! |
---|
Niemals primitive Datentypen als Body-Parameter verwenden! Die Werte können nicht zuverlässig an die Funktion des Service übergeben werden. Body Parameter sollten nur für Objekte und Arrays verwendet werden. |
Primitive Datentypen (und Felder) müssen als Body-Parameter in JSON Objekte verpackt werden!
Um das obere Beispiel lauffähig zu bekommen, gibt es einen einfachen Weg. Die primitiven Daten sollten in Objekte verpackt werden. Im folgenden Sample Code wurden für die primitiven Datentypen einfache Wrapper Objekte angelegt.
TextInput
fürstring
ParameterNumberInput
fürnumber
ParameterBoolInput
fürboolean
ParameterDateInput
fürDate
ParameterNumberArrayInput
fürnumber[]
ParameterBooleanArrayInput
fürboolean[]
Parameter
Der verbesserte Samplecode.
interface TextInput { value: string } interface NumberInput { value: number } interface BoolInput { value: boolean } interface DateInput { value: Date } interface NumberArrayInput { value: number[] } interface BoolArrayInput { value: boolean[] } @Route('Car') @injectable() export class CarController extends Controller { constructor () { super() } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/color') public async setColor (@Path('plateNr') plate: string, @Body() color: TextInput): Promise<void> { // ... } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/nextInspection') public async setNextInspection (@Path('plateNr') plate: string, @Body() date: DateInput): Promise<void> { // ... } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/mileage') public async setMileage (@Path('plateNr') plate: string, @Body() miles: NumberInput): Promise<void> { // ... } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/repair') public async setInRepair (@Path('plateNr') plate: string, @Body() repair: BoolInput): Promise<void> { // ... } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/parts') public async setPartsIds (@Path('plateNr') plate: string, @Body() ids: NumberArrayInput): Promise<void> { // ... } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/weekdayCleaning') public async setCleaningDays (@Path('plateNr') plate: string, @Body() days: BoolArrayInput): Promise<void> { // ... } }
Über die Swagger Console kann eine HTML Beschreibung und Testoberfläche für die generierte Schnittstelle mit deren Funktionen angezeigt werden.
- für alle Funktionen sind alle benötigten Eingabefelder vorhanden, die Funktionen können über die Oberfläche ausgeführt werden ✅
- die native Eingabeunterstützung für boolean, number, string und Date ist in der Oberfläche nicht nutzbar ❌
Ein Aufruf der primitiven Daten Setter ist jetzt auch über die URL möglich. Der Aufruf liefert Status Code 204 für eine erfolgreiche Ausführung zurück.
curl -X PUT "http://localhost:9091/v1/Car/DDAB123/color" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"value\": \"blue\"}"
Parameter Locations und optionale Parameter
Optionale Parameter werden je nach Parameter Location unterschiedlich unterstützt.
Query | ✅ vollständige Unterstützung |
---|---|
Path | ⚠ wird implizit als required markiert, Wert wird immer erwartet |
Body | ❌ Fehler bei Ausführung, Wert wird immer erwartet |
TSOA gestattet für alle Parameter Locations die Definition als optionalen Parameter. Die Generierung verläuft ohne Fehler und Warnungen.
@Tags('Garage') @Put('{nr}/{plate}') public async storeCar (@Body() car?: Car, @Path('plate') plate?: string, @Path('nr') nr?: number, @Query('lock') lock?: boolean): Promise<void> { // ... }
"/Car/{nr}/{plate}": { "put": { "operationId": "StoreCar", "produces": [ "application/json" ], "responses": { "204": { "description": "No content" } }, "tags": [ "Garage" ], "security": [], "parameters": [ { "in": "body", "name": "car", "required": false, "schema": { "$ref": "#/definitions/Car" } }, { "in": "path", "name": "plate", "required": true, "type": "string" }, { "in": "path", "name": "nr", "required": true, "format": "double", "type": "number" }, { "in": "query", "name": "lock", "required": false, "type": "boolean" } ] } }
Nach der Generierung des Services sind die optionalen Query Parameter auch in der Schnittstellenbeschreibung als optional markiert ("required": false
) ✅. Body und Path Parameter werden jedoch als required / benötigt behandelt ⚠.
Die generierte Swagger Beschreibung der Funktion zeigt, dass die Path Parameter nr und plate nicht mehr optional sind ("required": true
) ⚠. TSOA ändert das automatisch für Path Parameter während der Codegenerierung.
Auch der Body Parameter ist in der Praxis nicht mehr optional sondern muss angegeben werden ❌. Wird die obere Funktion ohne car aufgerufen
, dann wird ein Fehler zurückgemeldet.
- ohne Body Parameter wird Status Code 400 (Error: Bad Request) zurückgegeben
- ohne Body Parameter wird eine kryptische HTML Fehlermeldung zurückgegeben
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>[object Object]</pre> </body> </html>
Fazit |
---|
Path und BodyParameter können nicht als optional deklariert werden. |