Serialization of List<T> in POST request unsupported?

I am working on a gateway to interact with Pipedrive. The service looks like this:

[Route("/persons")]
public class CreatePipedrivePerson : IPost, IReturn<PipedriveCollection<PipedrivePerson>>
{
        [DataMember(Name = "email")] 
        public List<PipedriveEmail> Emails { get; set; } 
}

The corresponding PipedriveEmail class:

public class PipedriveEmail
{
    [IgnoreDataMember]
    public PipedriveType? Object { get; set; }
    
    public PipedriveEmail()
    {
        Object = PipedriveType.Email;
    }
    
    public string Label { get; set; }
    
    [DataMember(Name = "value")] 
    public string Email { get; set; }
    
    public bool Primary { get; set; }
}

The body of the request generated looks like this as far as I can tell:

name=Test&email={label:test1,value:test1%40test.com,primary:True},{label:test2,value:test2%40test.com,primary:False}

I think the format is not valid since the whole line ends up in the email: {label:test1,value:test1@test.com,primary:True},{label:test2,value:test2@test.com,primary:False} instead of being parse properly by the Pipedrive API.

The following curl request works as expected.

curl -H “Content-Type: application/json” -X POST -d

‘{“name”:“test”, “email”:[{“label”:“test1”, “value”:"test1@test.com", “primary”:“true”},{“label”:“test2”, “value”:"test2@test.com", “primary”:“false”}]}’ https://api.pipedrive.com/v1/persons?api_token=theapi_token

I’ve been doing a lot of research on the ServiceStack website but I can’t find the solution.

It looks like the [ ] are missing. Is this because List are not supported? Or because the body is formatted in Jsv and not json…? All the other types seemed to be working fine.

Any pointer would be much appreciated.

Can you provide the c# code making the request and the Corresponding raw HTTP Headers that’s failing?

Using POST should submit a serialized request (e.g in JSON), I can’t tell what’s generating the form URL encoded request you’re seeing.

Thanks for the prompt reply.

Here is the raw POST:

POST https://businessname-sandbox.pipedrive.com/v1/persons?api_token=the_token HTTP/1.1
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Accept: application/json
Accept-Encoding: gzip, deflate
Content-Length: 129
Host: businessname-sandbox.pipedrive.com

name=Test&email={label:test1,value:test1%40test.com,primary:True},{label:test2,value:test2%40test.com,primary:False}&visible_to=0

And the gateway code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Pipedrive.Bo;
using ServiceStack;
using ServiceStack.Text;

namespace Pipedrive
{
public class PipedriveGateway : IRestGateway
{
private const string BaseUrl = “Log in”;
private const string APIVersion = “v1”;

    public TimeSpan Timeout { get; set; }
    private string apiKey;
    private string UserAgent { get; set; }
    public PipedriveGateway(string apiKey)
    {
        this.apiKey = apiKey;
        Timeout = TimeSpan.FromSeconds(60);
        UserAgent = typeof(PipedriveGateway) + " v1";
        JsConfig.InitStatics();
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;//todo
    }
    protected virtual void InitRequest(HttpWebRequest req, string method)
    {
        req.Accept = MimeTypes.Json;
        if (method == HttpMethods.Post || method == HttpMethods.Put)
        {
            req.ContentType = MimeTypes.FormUrlEncoded;
        }
    }
    protected virtual void HandlePipedriveException(WebException ex)
    {
        string errorBody = ex.GetResponseBody();
        var errorStatus = ex.GetStatus() ?? HttpStatusCode.BadRequest;
        if (ex.IsAny400())
        {
            PipedriveError result = errorBody.FromJson<PipedriveError>();
            throw new PipedriveException(result)
            {
                StatusCode = errorStatus
            };
        }
    }
    protected virtual string Send(string relativeUrl, string method, string body)
    {
        try
        {
            var url = BaseUrl.CombineWith(relativeUrl);
            url = url.AddQueryParam("api_token", apiKey);//ma
            var response = url.SendStringToUrl(method: method, requestBody: body, requestFilter: req => { InitRequest(req, method); });
            return response;
        }
        catch (WebException ex)
        {
            HandlePipedriveException(ex);
            throw;
        }
    }
    protected virtual async Task<string> SendAsync(string relativeUrl, string method, string body)
    {
        try
        {
            var url = BaseUrl.CombineWith(relativeUrl);
            url = url.AddQueryParam("api_token", apiKey);//ma
            var response = await url.SendStringToUrlAsync(method: method, requestBody: body, requestFilter: req => { InitRequest(req, method); });
            return response;
        }
        catch (Exception ex)
        {
            if (ex.UnwrapIfSingleException() is WebException webEx)
                HandlePipedriveException(webEx);
            throw;
        }
    }
    public class ConfigScope : IDisposable
    {
        private readonly WriteComplexTypeDelegate holdQsStrategy;
        private readonly JsConfigScope jsConfigScope;
        public ConfigScope()
        {
            jsConfigScope = JsConfig.With(new Config
            {
                DateHandler = DateHandler.UnixTime,
                PropertyConvention = PropertyConvention.Lenient,
                EmitLowercaseUnderscoreNames = true,
                EmitCamelCaseNames = false,
                TreatEnumAsInteger = true
            });
            holdQsStrategy = QueryStringSerializer.ComplexTypeStrategy;
            QueryStringSerializer.ComplexTypeStrategy = QueryStringStrategy.FormUrlEncoded;
        }
        public void Dispose()
        {
            QueryStringSerializer.ComplexTypeStrategy = holdQsStrategy;
            jsConfigScope.Dispose();
        }
    }
    public T Send<T>(IReturn<T> request, string method, bool sendRequestBody = true)
    {
        using (new ConfigScope())
        {
            var relativeUrl = request.ToUrl(method);
            var body = sendRequestBody ? QueryStringSerializer.SerializeToString(request) : null; //original
            var json = Send(relativeUrl, method, body);
            var response = json.FromJson<T>();
            return response;
        }
    }
    public async Task<T> SendAsync<T>(IReturn<T> request, string method, bool sendRequestBody = true)
    {
        string relativeUrl;
        string body;
        using (new ConfigScope())
        {
            relativeUrl = request.ToUrl(method);
            body = sendRequestBody ? QueryStringSerializer.SerializeToString(request) : null;
        }
        var json = await SendAsync(relativeUrl, method, body);
        using (new ConfigScope())
        {
            var response = json.FromJson<T>();
            return response;
        }
    }
    private static string GetMethod<T>(IReturn<T> request)
    {
        var method = request is IPost ? HttpMethods.Post
            : request is IPut ? HttpMethods.Put
            : request is IDelete ? HttpMethods.Delete
            : HttpMethods.Get;
        return method;
    }
    public T Send<T>(IReturn<T> request)
    {
        var method = GetMethod(request);
        return Send(request, method, sendRequestBody: method == HttpMethods.Post || method == HttpMethods.Put);
    }
    public Task<T> SendAsync<T>(IReturn<T> request)
    {
        var method = GetMethod(request);
        return SendAsync(request, method, sendRequestBody: method == HttpMethods.Post || method == HttpMethods.Put);
    }
    public T Get<T>(IReturn<T> request)
    {
        return Send(request, HttpMethods.Get, sendRequestBody: false);
    }
    public Task<T> GetAsync<T>(IReturn<T> request)
    {
        return SendAsync(request, HttpMethods.Get, sendRequestBody: false);
    }
    public T Post<T> (IReturn<T> request)
    {
        return Send(request, HttpMethods.Post);
    }
    public Task<T> PostAsync<T>(IReturn<T> request)
    {
        return SendAsync(request, HttpMethods.Post);
    }
    public T Put<T>(IReturn<T> request)
    {
        return Send(request, HttpMethods.Put);
    }
    public Task<T> PutAsync<T>(IReturn<T> request)
    {
        return SendAsync(request, HttpMethods.Put);
    }
    public T Delete<T>(IReturn<T> request)
    {
        return Send(request, HttpMethods.Delete, sendRequestBody: false);
    }
    public Task<T> DeleteAsync<T>(IReturn<T> request)
    {
        return SendAsync(request, HttpMethods.Delete, sendRequestBody: false);
    }
}

}

And here is the API description link if needed: https://pipedrive.readme.io/docs/core-api-concepts-requests

Ok so you’re sending a custom HTTP Request instead of using a Service Client. So this isn’t to a ServiceStack Service then? If it is just use the C# Service Client and send typed DTOs, if it’s not you’d need to find out what serialization formats the API accepts as you shouldn’t be sending complex types using the HTTP default Form URL encoding as there is no standard for sending nested complex types. ServiceStack uses JSV which no 3rd party API is going to support natively.

Most APIs accept JSON (Which the API docs says it does) so you can try just sending JSON instead, e.g:

Serialize Request bodies to a JSON string instead:

var body = sendRequestBody ? request.ToJson() : null; //original

Then set JSON Content-Type:

var response = url.SendStringToUrl(method: method, requestBody: body, 
    contentType: MimeTypes.Json, accept: MimeTypes.Json,
    requestFilter: req => { InitRequest(req, method); });

This looks like you may have started with the StripeGateway which you likely shouldn’t have done, Stripe is very particular about its API which requires adhering to their specific conventions and data type formats which likely don’t apply to Pipedrive. e.g. given they accept JSON it should be preferred instead.

Either way as it’s not a ServiceStack Service you’re going to go through some trial and error to find out the JSON type formats they accept.

Hi. No, it’s not a service stack service…

I’m new to web services in general and the stripe project looked like a good example of what I was trying to accomplish…

I will try to make the changes you suggest. I’ve already tried some variations with no success.

Do you have another example besides the StripeGateway I could look at?

Thanks.

3rd Party Gateways are highly dependent on the API you’re trying to consume, the easiest solution is seeing if someone else has already created a c# binding to the pipedrives API and use that, otherwise here’s an example of a gateway I created around GitHub:

Or for Twitter:

Again highly dependent on the API you’re consuming so I’d recommend starting from a blank slate and build a Gateway that’s customized for the API you’re consuming.

With the latest modifications I’m now getting:

{"__type":“Pipedrive.CreatePipedrivePerson, Pipedrive”,“name”:“Test”,“email”:[{“label”:“test1”,“value”:“test1@test.com”,“primary”:true},{“label”:“test2”,“value”:“test2@test.com”,“primary”:false}]}

It looks like the namespace and class are now serialized in the body… Is there a configuration setting to avoid this?

If I manually remove this:

“__type”:“Pipedrive.CreatePipedrivePerson, Pipedrive”

It works perfectly.

I will check the other projects but everything was working almost perfectly.

Actually I change your code to:

var body = sendRequestBody ? request.GetDto().ToJson() : null;

And it works perfectly now.

2 Likes