Service Gateway not return Results (LIst<T>)

I am using ServiceStack 5.10.3 and I have two services defined as below

[Tag("Crypto")]
[Route("/crypto/transaction", "GET")]
public class QueryCryptoTx : IGet, IReturn<QueryCryptoTxResponse>
{
}

[Tag("Crypto")]
[Route("/crypto/transaction", "GET")]
public class GetCryptoTx : IGet, IReturn<GetCryptoTxResponse>
{
  public int Id { get; set; }
}

public class QueryCryptoTxResponse : IHasResponseStatus
{
    public List<CryptoTx> Results { get; set; } = new ();
    public ResponseStatus? ResponseStatus { get; set; } = null;
}

public class GetCryptoTxResponse : IHasResponseStatus
{
    public CryptoTx Result { get; set; } = new ();
    public ResponseStatus? ResponseStatus { get; set; } = null;
}

Both services access an external service, using my implementation of a ServiceStack Service Gateway, such that both services end up in the Send method below:

public T Send<T>(IReturn<T> request, string method, bool sendRequestBody = true, string? idempotencyKey = null)
{
    var relativeUrl = request.ToUrl(method);
    var body = sendRequestBody ? request.GetDto().ToJson() : null;
    var json = Send(relativeUrl, method, body, idempotencyKey);
    if (json.StartsWith("["))
    {
        json = "{" + json + "}";       // would fail is not wrapped with { } 
    }
    
    var response = json.FromJson<T>();               
    return response;
}

The service returning a single value (GetCryptoTx) works without issue; either populating the Result or the ResponseStatus, as expected.

The service returning a list of values (QueryCryptoTx) populates the ResponseStatus when there is an error. But, when there is no error, it is not populating the Results property:

{
“results”: [],
“responseStatus”: null
}

Incidentally, if I do not wrap the json with “{” and “}”, the json.FromJson() line, fails with a complaint that the json does not start with “{”.

{
  "results": [],
  "responseStatus": {
    "errorCode": "SerializationException",
    "message": "Type definitions should start with a '{', expecting serialized type 'QueryCryptoTxResponse', got string starting with: [{\"id\":1234,\"transactionOwner\":\"PRIVATE",
    "stackTrace": "[QueryCryptoTx: 2021-01-17 17:20:52]:\n[REQUEST: {}]\r\nSystem.Runtime.Serialization.SerializationException: Type definitions should start with a '{', expecting serialized type 'QueryCryptoTxResponse', got string starting with: [{\"id\":1234,\"transactionOwner\":\"PRIVATE\r\n   at ServiceStack.Text.Common.DeserializeTypeRefJson.StringToType(ReadOnlySpan`1 strType, TypeConfig typeConfig, EmptyCtorDelegate ctorFn, KeyValuePair`2[] typeAccessors) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\Common\\DeserializeTypeRefJson.cs:line 172\r\n   at ServiceStack.Text.Common.DeserializeType`1.StringToTypeContext.DeserializeJson(ReadOnlySpan`1 value) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\Common\\DeserializeType.cs:line 58\r\n   at ServiceStack.Text.Json.JsonReader`1.Parse(ReadOnlySpan`1 value) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\Json\\JsonReader.Generic.cs:line 110\r\n   at ServiceStack.Text.Json.JsonReader`1.Parse(String value) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\Json\\JsonReader.Generic.cs:line 83\r\n   at ServiceStack.Text.JsonSerializer.DeserializeFromString[T](String value) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\JsonSerializer.cs:line 47\r\n   at ServiceStack.StringExtensions.FromJson[T](String json) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\StringExtensions.cs:line 553\r\n   at ADG.ServiceInterface.TreasuryGateway.Send[T](IReturn`1 request, String method, Boolean sendRequestBody, String idempotencyKey) in C:\\Yoyo\\Alchemy\\ADG\\code\\backend\\ADG.ServiceInterface\\Treasury\\TreasuryGateway.cs:line 102\r\n   at ADG.ServiceInterface.TreasuryGateway.Send[T](IReturn`1 request) in C:\\Yoyo\\Alchemy\\ADG\\code\\backend\\ADG.ServiceInterface\\Treasury\\TreasuryGateway.cs:line 137\r\n   at ADG.ServiceInterface.TreasuryServices.Get(QueryCryptoTx request) in C:\\Yoyo\\Alchemy\\ADG\\code\\backend\\ADG.ServiceInterface\\Treasury\\TreasuryServices.cs:line 76\r\n   at ServiceStack.Host.ServiceRunner`1.ExecuteAsync(IRequest req, Object instance, TRequest requestDto) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack\\Host\\ServiceRunner.cs:line 134\r\n",
    "errors": [],
    "meta": null
  }
}

I am sure I have made mistakes, but could somebody explain:

  1. Why is the Results property not being populated from deserializing the Json array?

  2. Can I configure the Json deserializer to accept an array, so I do not need to wrap it as an object with “{” and “}”?

The service returning a list of values (QueryCryptoTx) populates the ResponseStatus when there is an error. But, when there is no error, it is not populating the Results property:

From your definition above, QueryCryptoTxResponse is an object not inheriting from a List so in JSON should be represented as an object { } not an array [ ]. If your trying to de-serialize an array into QueryCryptoTxResponse that won’t map correctly from a array. Wrapping an array response in curly braces is invalid JSON, eg { [ ] } so that will also cause problems.

If your internal service is returning an array and you want to de-serialize it into a DTO, that DTO will need to be an array/list type. Eg

public class QueryCryptoTxResponse : List<CryptoTx> {}

But you will lose ResponseStatus the DTO is now representing an array, not a single object message.

Alternatively you could handle the serialization yourself with JsConfig, eg

JsConfig<QueryCryptoTxResponse>.SerializeFn = response => { return ... };
JsConfig<QueryCryptoTxResponse>.DeSerializeFn = str => { return ... };

So 1. Json array response doesn’t map correctly to a DTO with a “Results” property and 2 see above.

Hope that helps.

Expanding on @layoric’s answer if your Service is returning an [] it’s not returning a serialized QueryCryptoTxResponse DTO which should look like {"results":[]}. Instead it’s likely returning a naked List, e.g. List<CryptoTx> which does not deserialize into a QueryCryptoTxResponse DTO.

To resolve it your Service should always return what you’re trying to deserialize into, in this case a serialized QueryCryptoTxResponse, not a JSON array like List<CryptoTx>.

Many thanks for your answers…

@layoric: following your suggestion by using

public class QueryCryptoTxResponse : List<CryptoTx> {}

worked, but at the loss of the ResponseStatus, as you pointed out.Your alternative of defining the JsConfig is not the path I want to follow, as I have numerous services like the above.

@mythz: adapting what you mentioned, I adjusted my special handling of the array case to add a results object:

    // special handling when array 
    if (json.StartsWith("["))
    {
        json = "{ \"results\": " + json + "}";
    }

This allowed me to keep my original definition of QueryCryptoTxResponse and have the Json deserialised into a Results property of type List, whilst keeping the ResponseStatus, which I need because the external service frequently unreliable.

Augmenting JSON responses is a hack, why can’t the external Service return what it says it does, i.e. a serialized QueryCryptoTxResponse DTO? Your Request DTOs are effectively lying about what’s being returned, maintaining hacks to fix the lie adds confusion and only resolves the discrepancy in this instance, it’s still going to break elsewhere the Request DTO is used to access the service.

Otherwise I’d modify your Request DTO to accurately reflect what the Service actually returns, e.g:

[Tag("Crypto")]
[Route("/crypto/transaction", "GET")]
public class QueryCryptoTx : IGet, IReturn<List<CryptoTx>>
{
}

Then your client can get the results without any hacks:

List<CryptoTx> results = Gateway.Send(new QueryCryptoTx());

ServiceStack Services returning naked DTOs still throw a Typed Service Exception which can be handled as normal.

Yes, you are completely right, so I reverted my hack. I now settled on your suggestion (and @layoric):

public class QueryCryptoTx : IGet, IReturn<List<CryptoTx>>

I also decided on doing the same for the single case, because I find it easier when my code is similar

public class GetCryptoTx : IGet, IReturn<CryptoTx>

With the ability to perform a check on the response using response.IsErrorResponse I achieve what I am after.

Thank you both.

1 Like