Swagger PUT/POST body value issue

We have enabled the Swagger plugin for ServiceStack and use it to submit test requests to our API. However, when we PUT or POST data via the Swagger UI, we see issues where Swagger is missing an extra JSON wrapper around the body parameter data after we click on the Model Schema to autofill the request data. Without that wrapper (see Request JSON Schema examples below), the call fails.

API Request C# Service Stack Class:

[Route("/assets/{AdID}/metadata", "PUT", Summary = @"Submit asset metadata.", Notes = "Add metadata for a new asset that will be in 'Pending Upload' status.")]
public class SubmitAssetMetadataRequest : IReturn<AssetMetadataResponse>
{
    [ApiMember(DataType = "string", IsRequired = true, ParameterType = "path", Description = "Asset ID")]
    public string AdId { get; set; }
    [ApiMember(DataType = "AssetMetadata", IsRequired = true, ParameterType = "body", Description = "Metadata payload for asset")]
    public AssetMetadata Metadata { get; set; }
}

Resulting Swagger UI:

Request JSON Schema (Default) - Fails:

{
    "AssetType": "Video",
    "AssetSubType": "BroadcastVideo",
    "AdvertiserId": 1111,
    "Title": "API Test Title",
    "Length": ":30",
}

Request JSON Schema (Manually Updated) – Succeeds:

{
    "Metadata" :
    {
        "AssetType": "Video",
        "AssetSubType": "BroadcastVideo",
        "AdvertiserId": 1111,
        "Title": "API Test Title",
        "Length": ":30",
    }
}

When we send the data via a client like Postman, we see the same behavior with the Metadata wrapper being required. Is there somethinig we need to change in our DTO definition or Swagger configuration to get the body JSON to be sent to the API properly?

Thanks!

Try removing the ParameterType=body

We’re experiencing exactly the same bug.
Removing ParameterType=body allows the Post function to read the values correctly, but breaks all of the DTO parameter handling in Swagger.
For example, if I have a method

    [Route("/workflow/definition", "POST")]
    public class WorkflowPostRequest: IReturn<Workflow>
    {
        [ApiMember(ParameterType = "body")]
        public Workflow workflow { get; set; }
    }

Swagger reports the call to this accepts a parameter of type Workflow. Perfect.
Templating from this generates a DTO parameter based function:
workflowPost(workflow: Workflow | null): Promise<Workflow>
Also perfect.

But while the JSON describes a format of body should be

{
  "Definition": "string",
  "Description": "string",
  "ProjectId": "string",
  "ScriptId": "string",
  "WorkflowId": "string"
 }

the ServiceStack library expects a JSON object of format

{ "workflow" :
 {
  "Definition": "string",
  "Description": "string",
  "ProjectId": "string",
  "ScriptId": "string",
  "WorkflowId": "string"
 }
}

which is wrong (there should be no leading ‘workflow’ key).
Removing ParameterType='body' results in a function that correctly accepts a JSON serialized Workflow object, but then implies a function parameter definition of
workflowPost_POST(definition: string | null, description: string | null, projectId: string, scriptId: string | null, workflowId: string, body: WorkflowPostRequest | null): Promise<Workflow>
which is completely wrong.
It looks like [ApiMember(ParameterType = "body")] is incorrectly retaining the name of the property its decorating, and including that in the generated definition.

One comment I read re the OpenAPI said “The name of the body parameter is not used in creating the request, it’s just used for documentation purposes.”
If that’s the case, it should not be used for deserialisation either.

Here’s my Swagger JSON for the problem endpoint:

    "/workflow/definition": {
      "post": {
        "tags": [
          "workflow"
        ],
        "operationId": "WorkflowPost_POST",
        "consumes": [
          "application/json"
        ],
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "workflow",
            "in": "body",
            "schema": {
              "$ref": "#/definitions/Workflow"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "schema": {
              "$ref": "#/definitions/Workflow"
            }
          }
        },
        "deprecated": false
      }

Incidentally, a slightly odd workaround to get to the values is to use:

    [Route("/workflow/definition", "POST")]
    public class WorkflowPostRequest: Workflow, IReturn<Workflow>
    {
        [ApiMember(ParameterType = "body")]
        public Workflow workflow { get; set; }
    }

(note that WorkflowPostRequest also inherits the Workflow class)

At least this way you can see the properties posted, the main downside being that it makes the Request object bloated with unwanted fields on server and in client generated libraries:

export interface WorkflowPostRequest {
    workflow?: Workflow | undefined;
    Definition?: string | undefined;
    Description?: string | undefined;
    ProjectId?: string | undefined;
    ScriptId?: string | undefined;
    WorkflowId?: string | undefined;
}

Declaration

[ApiMember(ParameterType = "body")]

Affects only Open API declaration it does not change behavior how ServiceStack handles requests. If you define

    [Route("/workflow/definition", "POST")]
    public class WorkflowPostRequest: IReturn<Workflow>
    {
        public Workflow workflow { get; set; }
    }

This means that ServiceStack will expect json with object Workflow inside.

{ "workflow" :
 {
  "Definition": "string",
  "Description": "string",
  "ProjectId": "string",
  "ScriptId": "string",
  "WorkflowId": "string"
 }
}

If you want to get all workflow properties as plain parameters

{
  "Definition": "string",
  "Description": "string",
  "ProjectId": "string",
  "ScriptId": "string",
  "WorkflowId": "string"
 }

you need to derive request from Workflow object as you do, but you can remove Workflow property inside it:

    [Route("/workflow/definition", "POST")]
    public class WorkflowPostRequest: Workflow, IReturn<Workflow>
    {
    }

Have you tried this and looked at the signature Swagger creates?
As I mentioned, if you do this you remove the signature that is used by swagger stating that a DTO is a parameter, and instead says that the path requires every single property of that DTO in it.
To solve this the SS deserialiser needs to read the metadata and recognise an [ApiMember(ParameterType = "body")] attribute on any member means the body must be serialised straight into that nested object.
ParameterType = "body" is only allowed on a single attribute, so there should be no issue with multiple objects trying to be read.
As you say, “Affects only Open API declaration it does not change behavior how ServiceStack handles requests”. That’s true, but that means the ServiceStack templates are generating a mismatch between the DTOs it declares and the Swagger signature it generates.
The signature I listed describes a call taking an interface as an argument, not taking the properties of that interface as parameters.

Can you provide a sample of the object what you are using and which shows mismatch between parameters of ServiceStack services and Open API definitions?

For example if use this DTO it does not requires to put every parameter, all of them are optional.

    public class Workflow
    {
        public int Id { get; set; }

        public string Name { get; set; }
    }

    [Route("/workflow/definition", "POST")]
    public class WorkflowPostRequest : Workflow, IReturn<Workflow>
    {
    }

Or do you want to remove query string params from Open API declaration (which are correctly mentioned here and can be used by API consumers)?

The parameter in the body of that POST operation should be of type Workflow.
What happens if you click on “Model” on the Swagger screen and check what the Swagger definition has defined it as?

    [Route("/workflow/definition1", "POST")]
    public class WorkflowPost1Request: Workflow, IReturn<Workflow>
    {
    }

So the body takes as its parameter a WorkflowPost1Request, not a workflow.
That means any API generated from this expects a parameter type of WorkflowPost1Request.
That’s clearly not what I want from my API, as I want to specify the DTO I want to pass in the body as a JSON object.
On top of that it now reports that I must provide every property used by a ‘Workflow’ as a parameter. That’s fine as an endpoint, but not what I want to advertise to users. I want to tell them they must provide a JSON object in the body, so their client APIs are simplified and simply work with the DTOs that Swagger also generates:

So I modify the API to allow me to pass the object in the body:

    [Route("/workflow/definition2", "POST")]
    public class WorkflowPost2Request: IReturn<Workflow>
    {
        [ApiMember(ParameterType = "body")]
        public Workflow workflow { get; set; }
    }

Much better:

Oh… but when I look at an example value to write into the body, it’s actually a collection of properties (as I’d expect, after all, this is the body ‘property’), and the SS API will not recognise it’s own generated definition.

So obviously both of these are wrong. OpenAPI states you must have only a single ‘body’ source, but multiple ‘path’ sources. If I want to advertise an API accepting a parameter of type ‘Workflow’, that should be perfectly valid as it’s already reported that way in the generated JSON, but not recognised by the SS request deserialiser (because it doesn’t realise the entire body is a single property).
It’s not a simple problem, but if you can’t click ‘Try It Out’ on the systems own interface, something is mismatched.

I’ll reiterate what @xplicit said, the Open API definition has no effect on the serialization which needs to match exactly the schema of the Request and Response DTO. The purpose of Open API is to describe your Service API, it is not meant to custimize it, it has no effect on how the Request is deserialized and if your description doesn’t match the Request/Response DTOs, the clients that use the Open API description will send an invalid Request.

OK, so are we saying the ServiceStack Swagger output is deliberately wrong?
If we can’t click on the button in the built-in swagger-ui to inject back in its own defined object, there is something amiss.
You’re already modifying the behaviour by ensuring [ApiMember(ParameterType = "body")] prevents the interface publishing the properties of the parent Request object.
Now when the request arrives, the system currently assumes that if it has a property called ‘X’ in the request body, it will look through all of the fields in the body and path for any matching key with the same name.
That’s great, and works nicely.
Now when we have a property flagged with [ApiMember(ParameterType = "body")], we’re saying "When you generate a signature for this request, consider it as ‘the body == X’.
So when it comes to deserialisation of the packet, when looking at the destination Request object to populate, if a member property is decorated with [ApiMember(ParameterType = "body")] the system must drill into that property, and populate its properties with the fields, not just the ones at the root of the Request object.

I understand exactly what you’re saying, but to be support the OpenAPI definition the decoding of an incoming body has to match the definition, and a property marked as ParameterType = "body" is not carried by a variable in the OpenAPI standard (it is ‘the body’), which is why the current implementation fails when it needs to deserialise it.

If you’re customizing the Swagger output with [Api] and [ApiMember] attributes then it’s up to you to ensure that your Swagger definition accurately represents what your Service accepts/returns. Decorating a DTO property with [ApiMember(ParameterType = "body")] almost always does not represent the schema of the Service unless you’ve registered a custom Request Binder or your Request DTO implements IRequiresRequestStream where you take over responsibility of handling the request.

So to be clear again, the purpose of Swagger annotations is to describe your HTTP API contract, it is not for you to try change how ServiceStack should handle the request, Api metadata annotations have no influence on how requests are deserialized. The Services Contract is a one-way relationship which generates the Swagger spec from its schema which you can customize with annotations and filters, but those customizations do not go back and have any effect on how the Request is handled, it’s up to you to ensure your customizations accurately reflect your Services contract.

Sorry, these posts are getting so long…

OK, I understand Swagger is a descriptor of the API, not a definer, and I’m completely open to this.
How do I define a Swagger published API endpoint, advertising a DTO based parameter of type workflow?
Bear in mind that I might also have to allow path variables (for example a ProjectId), although according to the OpenAPI stuff, they would have to not be in the body if another parameter is already defined as ‘in body’.

My intended API use is to say SaveWorkflow_POST(Workflow data,Guid projectid)

In non-Swagger-API-terms my assumption is that I would define my Request object as

[Route("/{ProjectId}/workflow", "POST")]
    public class WorkflowPostRequest: Workflow, IReturn<Workflow>
    {
        public Guid ProjectId { get; set; }
    }

This does exactly what I require, is the way I’ve always used SS APIs, and I can simply send a POST to /projidguid/workflow with a body containing a json object such as

{
 "name":"workflow1",
 "description","Awesome workflow, Jason!"
 "definition": { "x":"1","y":"2" }
 "actions": [ "blah", "blah" ]
}

Now, I want to describe this in Swagger. How do I do this? Can you show me an example of the attributes required to automatically generate it?
I don’t want a signature of
SaveWorkflow_POST(string name, string description, DefObject definition, string[] actions, Guid projectid)
I simply want
SaveWorkflow_POST(Workflow data,Guid projectid)

Here are some of the experiments we tried to solve it ourselves:

Inheriting from Workflow simply provides a dummy object so that ServiceStack has somewhere to dump the body object data into, rendering the internal workflow property redundant, but critical for the Swagger API generation:

    [Route("/{ProjectId}/workflow", "POST")]
    public class WorkflowPostRequest: Workflow, IReturn<Workflow>
    {
        [ApiMember(ParameterType = "path")]
        public Guid ProjectId { get; set; }
        [ApiMember(ParameterType = "body")]
        public Workflow workflow { get; set; }   <*** This is redundant, but essential for the Swagger signature
    }

The extra ‘dummy’ workflow is the only way to have the system export a valid parameter with type Workflow.

We want

    [Route("/{ProjectId}/workflow", "POST")]
    public class WorkflowPostRequest: IReturn<Workflow>
    {
        [ApiMember(ParameterType = "path")]
        public Guid ProjectId { get; set; }
        [ApiMember(ParameterType = "body")]
        public Workflow workflow { get; set; }
    }

Adding [ApiMember(ParameterType = “body”)] to any member tells the OperationFilter not to output the properties of the base class, but to only export OpenAPI attributed parameters. The “body” one is effectively ‘promoted’ to be the only item populatable from the body of the request, thus implying that the request body itself is a serialised instance of a Workflow.

How else can I do this?
Please actually try this in swagger-ui.
Here is exactly what a definition described above should look like:


Note that the sample data is not (and should not be)

"workflow" {
 {
  "Definition": "string",
  "Description": "string",
  "ProjectId": "string",
  "ScriptId": "string",
  "WorkflowId": "string"
 }
}

It should not have "workflow": because that is not defined in the argument list of the Swagger JSON, which is also why your '“Example Value” on the right does not contain it, but doesn’t work.

So my question is; is your sample value incorrectly generated, or is your Swagger definition of the Model itself wrong?
Let me somehow move the [ApiMember(ParameterType = "body")] onto the WorkflowRequest object, and tell Swagger to advertise it as a body parameter of type Workflow and I believe this would all be correct :smile:

It looks to be a real problem, impacting people using swagger JSON based tools such as NSwagStudio to create client APIs with POST/PUT/PATCH data in a body.
I suspect that whatever you decide to resolve the SS swagger-ui bug Example Code bug, you’ll also have resolved this issue.

If you want to use

SaveWorkflow_POST(string name, string description, DefObject definition, string[] actions, Guid projectid)

instead of

SaveWorkflow_POST(Workflow data,Guid projectid)

you should use this DTO

public class WorkflowPostRequest: IReturn<Workflow>
    {
        public Guid ProjectId { get; set; }

        public Workflow workflow { get; set; }
    }

instead of deriving request from Workflow class. And in that case Workflow parameter will be incapsulated in Open API parameter workflow and in this case swagger ui generates correct sample which is shown in your post.

"workflow" {
 {
  "Definition": "string",
  "Description": "string",
  "ProjectId": "string",
  "ScriptId": "string",
  "WorkflowId": "string"
 }
}

If you use DTO derived from other class it’s equal to this semantic:

SaveWorkflow_POST(string name, string description, DefObject definition, string[] actions, Guid projectid)
    [Route("/workflow/{ProjectId}", "POST")]
    [Api(BodyParameter = GenerateBodyParameter.Always, IsRequired = true)]
    public class WorkflowPostRequest : Workflow, IReturn<Workflow>
    {
        [ApiMember(ParameterType = "path", ExcludeInSchema = true)]
        public string ProjectId { get; set; }
    }

Where model will be

WorkflowPostRequest : {
   ...
}

And parameter Model is WorkflowPostRequest because ServiceStack handler expect WorkflowPostRequest as parameter, not Workflow and the definition is generated correctly too.

Do I correctly understand that if request is derived from another class you want to rename model name which is shown in swagger and declare all parameter from base class as body parameter while declare parameters of derived class as query/path params?

Do I correctly understand that if request is derived from another class you want to rename model name which is shown in swagger and declare all parameter from base class as body parameter while declare parameters of derived class as query/path params?

That’s pretty much it. It’s a very simple and typical usage scenario, where a new Workflow is created by POSTing a json serialised Workflow. Swagger is configured to say that the API expects a body containing an object of type Workflow, using a DTO that is also defined in the same Swagger definition file.
The SS swagger definition contains the DTOs, but in the code above it has identified that this API expects a WorkflowPostRequest object, rather than a Workflow. The Swagger definition correctly specifies ProjectId as an additional parameter.
So as such I guess it’s not a rename of ‘model name’, and more a specifying of which class (i.e. DTO) the body represents in the Swagger model. So where the Parameter on your example for ‘body’ says WorkflowPostRequest, in this case we’d want it to be Workflow.

OK, I’m giving up on this for now.
The fix of renaming the return type doesn’t work. The idea of inheriting from Workflow causes all sorts of issues when a URL parameter is also required, as we end up with shadowed properties.
When we inherit from Workflow we get every property defined as a parameter.
When we try and use a nesting object to hold both ProjectId and Workflow the Swagger definition is huge and messy, describing Workflow as type ‘query’ of type string, WorkflowPostRequest as the body, making API generation awful.
When we describe any parameter as ApiMember the definition messes up the body parameter description.

I’ve modded the generation to strip all ‘query’ parameters, which helps a tiny bit, and we’ll come back to this some time in the future and see if it’s been fixed up.

You can use [ApiMember(ExcludeInSchema=true)] and [ApiMember(ParameterType=“model”)] to exclude properties you don’t want to see in Open API definitions. For example

    [Route("/workflow/{ProjectId}", "POST")]
    [Api(BodyParameter = GenerateBodyParameter.Always, IsRequired = true)]
    public class WorkflowPostRequest : IReturn<Workflow>
    {
        [ApiMember(ParameterType = "path", ExcludeInSchema = true)]
        public string ProjectId { get; set; }

        [ApiMember(ParameterType = "model")]
        public Workflow Workflow { get; set; }
    }

will generate this Open API definition:

You can see that “query” param for workflow is removed from parameters, and body definition does not contain ProjectId which is defined as parameter only.