When I send a request to a service (that I do not own), it may respond either with the JSON data requested, or with an error that looks like this:
{
"error": {
"status": "error message",
"code": "999"
}
}
In both cases the HTTP response code is 200 OK, so I cannot use that to determine whether there is an error or not - I have to deserialize the response to check. So I have something that looks like this:
bool TryParseResponseToError(string jsonResponse, out Error error)
{
// Check expected error keywords presence
// before try clause to avoid catch performance drawbacks
if (jsonResponse.Contains("error") &&
jsonResponse.Contains("status") &&
jsonResponse.Contains("code"))
{
try
{
error = new JsonSerializer<Error>().DeserializeFromString(jsonResponse);
return true;
}
catch
{
// The JSON response seemed to be an error, but failed to deserialize.
// Or, it may be a successful JSON response: do nothing.
}
}
error = null;
return false;
}
Here, I have an empty catch clause that may be in the standard execution path, which is a bad smell... Well, more than a bad smell: it stinks.
Do you know a better way to "TryParse" the response in order to avoid a catch in the standard execution path ?
[EDIT]
Thanks to Yuval Itzchakov's answer I improved my method like that :
bool TryParseResponse(string jsonResponse, out Error error)
{
// Check expected error keywords presence :
if (!jsonResponse.Contains("error") ||
!jsonResponse.Contains("status") ||
!jsonResponse.Contains("code"))
{
error = null;
return false;
}
// Check json schema :
const string errorJsonSchema =
@"{
'type': 'object',
'properties': {
'error': {'type':'object'},
'status': {'type': 'string'},
'code': {'type': 'string'}
},
'additionalProperties': false
}";
JsonSchema schema = JsonSchema.Parse(errorJsonSchema);
JObject jsonObject = JObject.Parse(jsonResponse);
if (!jsonObject.IsValid(schema))
{
error = null;
return false;
}
// Try to deserialize :
try
{
error = new JsonSerializer<Error>.DeserializeFromString(jsonResponse);
return true;
}
catch
{
// The JSON response seemed to be an error, but failed to deserialize.
// This case should not occur...
error = null;
return false;
}
}
I kept the catch clause... just in case.
You may deserialize JSON to a
dynamic
, and check whether the root element iserror
. Note that you probably don't have to check for the presence ofstatus
andcode
, like you actually do, unless the server also sends valid non-error responses inside aerror
node.Aside that, I don't think you can do better than a
try/catch
.What actually stinks is that the server sends an HTTP 200 to indicate an error.
try/catch
appears simply as checking of inputs.A slightly modified version of @Yuval's answer.
This can be used when you don't have the schema as text readily available for any type.
With
Json.NET
you can validate your json against a schema:And then use that inside a TryParse method.
Then do:
Update:
Please note that schema validation is no longer part of the main Newtonsoft.Json package, you'll need to add the Newtonsoft.Json.Schema package.
Update 2:
As noted in the comments, "JSONSchema" have a pricing model, meaning it isn't free. You can find all the information here
Just to provide an example of the try/catch approach (it may be useful to somebody).
Then, it can be used like this:
To test whether a text is valid JSON regardless of schema, you could also do a check on the number of quotation marks:" in your string response, as shown below :
@Victor LG's answer using Newtonsoft is close, but it doesn't technically avoid the a catch as the original poster requested. It just moves it elsewhere. Also, though it creates a settings instance to enable catching missing members, those settings aren't passed to the DeserializeObject call so they are actually ignored.
Here's a "catch free" version of his extension method that also includes the missing members flag. The key to avoiding the catch is setting the
Error
property of the settings object to a lambda which then sets a flag to indicate failure and clears the error so it doesn't cause an exception.Here's an example to use it: