OpenApi how to define with mixed body and query parameters?

Hi. I am reverse engineering a possible client/partners’ rest API, recreating it in SS so that I can
a) use my local instance as a proxy API with 1-to-1 message passthrough, and more importantly:
b) generate an OpenAPI definition for importing into Azure API Management / Logic Apps, generate consumer client SDKs etc . The idea is to be able to provide their clients with integrations between Netigate and client, but because Netigate don’t currently provide a Swagger doc, this is a tedious process, so as step one, I am recreating their endpoints based on their documentation so that I can build the OpenApi spec, and go from there.

However, I cannot figure out how to make ServiceStack’s implementation of OpenAPI match their API definition AND also meet the OpenAPI spec. As a demo, I have created a public feature branch on github, where I demonstrate the issue, available here

Basically, because I have to include the PATH params in my servicestack request class, I don’t know how to then make SS generate the BODY part of my swagger/OpenAPI definition correctly, when there are several properties to be included.

This might be straightforward to solve, but I’ve been staring at this for a while now and cannot see a solution? I.e make a ServiceStack request class, which contains properties that are used in path, query AND body?

I cannot find any samples of this either, so if you have and can point me to them, my apologies in advance.

If you spin up that project and go to swagger, you should see a single endpoint, which is supposed to correspond to the netigate API defined here

The thing is, Netigate have path parameters, query parameters AND (critically) multiple properties in the PUT body, and its that last part that causes me issues.

Looking at their API endpoint definition, there is surveyId and sendoutId as path params, which I have included in my AddRespondentRequest class and they appear in the generated OpenAPI documentation correctly.

However, the remaining three properties in that AddRespondentRequest class are causing me problems.

As per their documentation at the above link, Netigate expect two path params, and then input body to look like:

{
"contactDetails": "foo@bar.com",
"sendMail": true,
"backgroundData": [
    {
        "Key": "12345",
        "Value": "Value1"
    },
    {
        "Key": "12346",
        "Value": "Value2"
    },
    {
        "Key": "12347",
        "Value": "Value3"
    }
]
}

However, according to OpenApi 2.0 (which SS is using) I can only have a single property in my request class specified as “body”, which in my case is the

[ApiMember(Name = "backgroundData",
		Description = "Remarks: Key = BGDataLabelId, Value = respondent's background data (not empty or null)",
		ParameterType = "body",
		//DataType = "object",
		IsRequired = false
	)]
	public Dictionary<int, string> backgroundData { get; set; } 

but that leaves me with two remaining properties:

[ApiMember(Name = "contactDetails",
		Description = "Remarks: Valid email address, SMS recipient or login identifier.",
		ParameterType = "query",
		DataType = "string",
		IsRequired = true
	)]
	public string contactDetails { get; set; }

	[ApiMember(Name = "sendMail",
		Description = "Remarks: Indicates whether Netigate should send the survey link to the respondent, or if you distribute it yourself.",
		ParameterType = "query",
		DataType = "boolean",
		IsRequired = true
	)]
	public bool sendMail { get; set; }

that SHOULD be sent as part of the body, (NOT the query), but that I can’t figure out how to configure (at the moment they are in my class defined as ParameterType=query, which generates a valid OpenApi spec file, using the online validator here) but that spec does NOT correctly/accurately match the intended Netigate documentation and API implementation).

What is the Open API v2 Spec definition do you need to generate for this Service?

BTW this link is broken: https://apidevtools.org/swagger-parser/online/

Anyway the Open API support is for exposing your Services that ServiceStack accepts, if you need to try to conform to someone else’s API definition, you’ll likely need to use one of the Operation Filters to enable fine-grain customization of the Open API definition. The most important thing to know is exactly what Open API definition that you need to generate.

Hi, thanks for replying on the weekend! Please don’t use your weekend for this however.

For later:

Re: the broken validator url - weird, i have been using that for two days now and suddenly the entire site is gone?

Re: definitions - Sorry, I am not sure exactly what you mean by “know what API definition i need”?

The endpoint in question is a POST operation, modelled by the AddRespondentRequest class. That single request class that I created, contains two properties used in the url path, and then three remaining properties that should be posted as the body of the POST request (and indeed, this works fine when I actually post data through to Netigate’s API… the path is build correctly, and body content is all properties serialized, eg:

{  
    "surveyId":778626,   // so this is technically redundant and not required in body
    "sendoutId":289258, // same ^^ here
    "contactDetails":"dale.holborow@eladaus.com", // mandatory, in body
    "sendMail":true, // mandatory, in body
    "backgroundData":null // an option param, in body when included
}

and I get a successful response back from their api - so far so good, in terms of functionality.

The confusion I have about marking up request message classes for OpenApi (the definition of which I want to later import into Azure, for example) is that, in my mind, the entire AddRespondantRequest class should effectively be considered the post “body” as all properties are serialized as application/json and delivered in the http request’s body, (or ideally, entire class minus those two properties used in the path), as opposed to currently where I am stuck only able to assign “ParameterType=body” to a single property WITHIN that class, using the ApiMemberAttribute.

I.e. It feels like there’s a mismatch between how the request class is actually serialized for functionality (in its entirety, in the http request Body section) vs how I am able to mark it up for OpenApi (where paramtype=body can only be specified on a single parameter within the class)

Sorry if this comes across as repetitive, I’m struggling to explain myself.

The Open API Specification returns a JSON format that’s serialized using these properties, ServiceStack contains Typed DTO’s that maps 1:1 to the v2 specification, so using the available filters you should be able to return the custom Open API definition for the operation you need.

There can only be 1 body, which should be left as the Request DTO, that represents the Request body. The parameters in the /path/info are excluded in the body. For the other parameters you can make them optional Query String parameters, where they can be populated on the QueryString or sent in the Request Body as JSON, so you could try this:

[Route(
    "/surveys/{surveyId}/sendouts/{sendoutId}/respondents",
    HttpMethods.Post,
    Summary = "Adding Respondents",
    Notes = "Add a new Respondent with optional background data to a Sendout."
)]
//[Exclude(Feature.Metadata)]	// hide from OpenAPI
//[RateLimitedPerUser]
public class AddRespondentRequest : IReturn<AddRespondentResponse>
{
    [ApiMember(Name = "surveyId",
        Description = "Remarks: SurveyId of requested Survey.",
        ParameterType = "path",
        DataType = "integer", Format = "int32",
        IsRequired = true
    )]
    public int surveyId { get; set; }

    [ApiMember(Name = "sendoutId",
        Description = "Remarks: SendoutId of Sendout to which a respondent is added.",
        ParameterType = "path",
        DataType = "integer", 
        Format = "int32",
        IsRequired = true
    )]
    public int sendoutId { get; set; }

    [ApiMember(Name = "contactDetails",
        Description = "Remarks: Valid email address, SMS recipient or login identifier.",
        ParameterType = "query",
        DataType = "string",
        IsRequired = false
    )]
    public string contactDetails { get; set; }

    [ApiMember(Name = "sendMail",
        Description = "Remarks: Indicates whether Netigate should send the survey link to the respondent, or if you distribute it yourself.",
        DataType = "boolean",
        ParameterType = "query",
        IsRequired = false
    )]
    public bool sendMail { get; set; }

    [ApiMember(Name = "backgroundData",
        Description = "Remarks: Key = BGDataLabelId, Value = respondent's background data (not empty or null)",
        ParameterType = "query",
        //DataType = "object",
        IsRequired = false
    )]
    public Dictionary<int, string> backgroundData { get; set; }
}

If the purpose is just to have a Typed API it should be easier if you skip Open API altogether and use the code-first approach that ServiceStack.Stripe uses to provide a Typed API for Stripe’s 3rd Party Rest API as captured in the StripeGateway.cs DTOs.

Thanks for this - I ended up using more or less this approach. My option seemed to be either specify contactdetails/sendmail/backgrounddata as “query” so that their descriptions etc would be included in the openapi json in the parameters section, or NOT put any ApiMember attribute on them at all, in which case they’d correctly appear in the “body” section but missing the convenient documentation… so i settled for marking them as query (as i want to keep the description, required=true/false info etc when using Logic Apps to do integrations).

I have actually studied your Stripe API a while ago, and started a couple of similar “gateway”-type projects, its just that this time I specifically wanted/needed to make my code a 1-to-1 “proxy” rather than “gateway”, basically to be able to import the openapi file into Azure Logic Custom Connectors, and also to potentially provide to Netigate so as to fix/improve their existing API documentation.

Thanks again!

1 Like