Question:
Using ServiceStack, is it possible to validate JSON data before it is mapped (by ServiceStack) to a DTO?
Example:
My DTO Shape:
public class ExampleDto
{
public int? MyValue {get;set;}
}
Example (probalamatic) payload:
{
"MyValue": "BOB"
}
Problem:
The problem for me is that the consumer of my API has not looked at the documentation properly, and is trying to pass through a string, where the ServiceStack mapping will be expecting to map a nullable integer. This just comes through as NULL.
I use the really cool validation feature in my API, but that only kicks in after the data (passed in by the consumer of my API) is mapped to the DTO. As far as I can see, it doesn't see that the user attempted to pass through a value that couldn't be mapped to the DTO.
Is there any way in ServiceStack to validate any potential serialisation errors?
Ideally, I'd like to be able to return the mismatching serialisation in the same list of errors that the FluentValidation feature returns for consistency, but I'd settle for not allowing an end user to be able to make this kind of request at all.
CodePudding user response:
Update: to make it easier to support this scenario I've added support for overriding JSON Deserialization in this commit where you'll be able to intercept JSON deserialization by overriding OnDeserializeJson()
in your AppHost, e.g:
class AppHost : AppHostBase
{
//..
public override object OnDeserializeJson(Type intoType, Stream fromStream)
{
if (MyShouldValidate(intoType))
{
var ms = MemoryStreamFactory.GetStream();
fromStream.CopyTo(ms); // copy forward-only stream
ms.Position = 0;
var json = ms.ReadToEnd();
// validate json...
fromStream = ms; // use buffer
}
return base.OnDeserializeJson(intoType, fromStream);
}
}
This change is available from v5.12.1 that's now available on MyGet.
This can also be accomplished in earlier versions by registering a custom JSON Format which is effectively what this does by routing the existing JSON Deserialization method to the overridable OnDeserializeJson()
.
You should refer to the Order of Operation docs to find out what custom hooks are available before deserialization which are just:
- The
IAppHost.PreRequestFilters
gets executed before the Request DTO is deserialized - Default Request DTO Binding or Custom Request Binding (if registered)
But if you want to read the JSON Request Body in a PreRequestFilters
you'll need to Buffer the Request then you can read the JSON to deserialize and validate it yourself.
appHost.PreRequestFilters.Add((httpReq, httpRes) => {
if (!httpReq.PathInfo.StartsWith("/my-example")) continue;
httpReq.UseBufferedStream = true; // Buffer Request Input
var json = httpReq.GetRawBody();
// validate JSON ...
if (badJson) {
//Write directly to Response
httpRes.StatusCode = (int)HttpStatusCode.BadRequest;
httpRes.StatusDescription = "Bad Request, see docs: ...";
httpRes.EndRequest();
//Alternative: Write Exception Response
//httpRes.WriteError(new ArgumentException("Bad Field","field"));
}
});
If the request is valid JSON, but just invalid for the type you could parse the JSON into an untyped dictionary and inspect its values that way with JS Utils, e.g:
try {
var obj = (Dictionary<string, object>) JSON.parse(json);
} catch (Exception ex) {
// invalid JSON
}
Alternatively you could take over deserialization of the request with a Custom Request Binding, i.e:
base.RegisterRequestBinder<ExampleDto>(httpReq => ... /*ExampleDto*/);
That throws its own Exception, although you should be mindful that ServiceStack APIs deserializes its Request DTO from multiple sources so if you're only checking a JSON Request Body its going to ignore all the other ways an API can be called.
The other way to take over deserialization of the Request DTO is to have your Service read directly from the Request Stream by having your Request DTO implement IRequiresRequestStream
which tells ServiceStack to skip deserialization so your Service could do it and manually validate it, e.g:
public class ExampleDto : IRequiresRequestStream
{
public Stream RequestStream { get; set; }
public int? MyValue {get;set;}
}
public class MyServices : Service
{
public IValidator<ExampleDto> Validator { get; set; }
public async Task<object> PostAsync(ExampleDto request)
{
var json = await request.RequestStream.ReadToEndAsync();
// validate JSON...
// deserialize into request DTO and validate manuallly
var dto = json.FromJson<ExampleDto>();
await Validator.ValidateAndThrowAsync(dto, ApplyTo.Post);
}
}