104

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.

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
Dude Pascalou
  • 2,989
  • 4
  • 29
  • 34

7 Answers7

88

@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.

 public static bool TryParseJson<T>(this string @this, out T result)
 {
    bool success = true;
    var settings = new JsonSerializerSettings
    {
        Error = (sender, args) => { success = false; args.ErrorContext.Handled = true; },
        MissingMemberHandling = MissingMemberHandling.Error
    };
    result = JsonConvert.DeserializeObject<T>(@this, settings);
    return success;
}

Here's an example to use it:

if(value.TryParseJson(out MyType result))
{ 
    // Do something with result…
}
Steve In CO
  • 5,746
  • 2
  • 21
  • 32
  • 4
    Note that this returns true for empty objects (e.g. "{}") unless you use [JsonProperty(Required = Required.Always)] on your result class. – user764754 Aug 27 '18 at 12:55
  • @user764754 But that's not unique to this implementation. Calling `JsonConvert.DeserializeObject("{}")` will not throw an exception unless `T` is an array type (or a property is required, as you said). – Gabriel Luci Jul 19 '19 at 14:09
  • 4
    Technically, this answer does not necessarily avoid the catch either. If you pass invalid JSON to this method then JsonConvert.DeserializeObject will throw an error and catch it internally. In that case, this method just moves the catch elsewhere. – asthomas Nov 21 '19 at 14:37
  • This fails when date is`DateTime.MinValue`. We can still use `if (!DateTime.TryParse(strValue, out DateTime date))` or set date format handling https://stackoverflow.com/a/21616344/591285 – clamchoda Nov 25 '22 at 16:30
64

With Json.NET you can validate your json against a schema:

 string schemaJson = @"{
 'status': {'type': 'string'},
 'error': {'type': 'string'},
 'code': {'type': 'string'}
}";

JsonSchema schema = JsonSchema.Parse(schemaJson);

JObject jobj = JObject.Parse(yourJsonHere);
if (jobj.IsValid(schema))
{
    // Do stuff
}

And then use that inside a TryParse method.

public static T TryParseJson<T>(this string json, string schema) where T : new()
{
    JsonSchema parsedSchema = JsonSchema.Parse(schema);
    JObject jObject = JObject.Parse(json);

    return jObject.IsValid(parsedSchema) ? 
        JsonConvert.DeserializeObject<T>(json) : default(T);
}

Then do:

var myType = myJsonString.TryParseJson<AwsomeType>(schema);

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

Hassan Faghihi
  • 1,888
  • 1
  • 37
  • 55
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
33

A slightly modified version of @Yuval's answer.

static T TryParse<T>(string jsonData) where T : new()
{
  JSchemaGenerator generator = new JSchemaGenerator();
  JSchema parsedSchema = generator.Generate(typeof(T));
  JObject jObject = JObject.Parse(jsonData);

  return jObject.IsValid(parsedSchema) ?
      JsonConvert.DeserializeObject<T>(jsonData) : default(T);
}

This can be used when you don't have the schema as text readily available for any type.

M22an
  • 1,242
  • 1
  • 18
  • 35
  • Curious if this: JsonConvert.DeserializeObject(jsonData) is better thanjust doing (T)jObject . You already deserialized once to JObject, so it seems like a cast might be cheaper? I really don't know. – solvingJ Mar 17 '17 at 19:17
  • 1
    Also in many cases if you don't control the JSON it might be wise to wrap the JObject.Parse() in a separate method with a separate try/catch since it throws exception for invalid JSON. There are two distinct conditions that could happen here, 1) Invalid JSON, 2) Json doesn't match the schema you expect. In our case we handle differently. – solvingJ Mar 17 '17 at 19:22
  • My first question about deserializing vs casting is invalid. Should have said: jObject.ToObject() – solvingJ Mar 17 '17 at 19:24
  • @solvingJ, handling invalid JSON and JSON not matching schema can be handled separately, but for our case we are not expecting invalid JSON and that is handled in a different layer so this makes sense for us. This `JObject jObject = JObject.Parse(jsonData);` check is redundant for our case. – M22an Apr 05 '17 at 11:49
30

Just to provide an example of the try/catch approach (it may be useful to somebody).

public static bool TryParseJson<T>(this string obj, out T result)
{
    try
    {
        // Validate missing fields of object
        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.MissingMemberHandling = MissingMemberHandling.Error;

        result = JsonConvert.DeserializeObject<T>(obj, settings);
        return true;
    }
    catch (Exception)
    {
        result = default(T);
        return false;
    }
}

Then, it can be used like this:

var result = default(MyObject);
bool isValidObject = jsonString.TryParseJson<MyObject>(out result);

if(isValidObject)
{
    // Do something
}
NYCdotNet
  • 4,500
  • 1
  • 25
  • 27
Victor LG
  • 606
  • 6
  • 7
  • 1
    This doesn't seem to work. DeserializeObject does not throw an exception if it's valid json which doesn't match the object. Using this method to solve the orginal question will require further validation. – Derrick Jul 06 '18 at 18:22
  • 2
    You are totally right @Derrick, thank you for pointing that out. I just updated my answer to validate when there are missing fields of the object to be deserialized. Note: Credits of the update based on this answer: https://stackoverflow.com/questions/21030712/can-you-detect-if-an-object-you-deserialized-was-missing-a-field-with-the-jsonco – Victor LG Jul 06 '18 at 21:54
  • 1
    Still got a typo in your code sample old chap. Should be `result = JsonConvert.DeserializeObject(obj, settings);` - other than that, thanks for sharing, a nice solution that helped me out :) – Richard Moore Jan 25 '19 at 16:41
  • 2
    Also changed `catch (JsonSerializationException ex) ` to `catch (Exception)` so that it returns false if invalid Json is passed to it. – Richard Moore Jan 25 '19 at 16:51
3

You may deserialize JSON to a dynamic, and check whether the root element is error. Note that you probably don't have to check for the presence of status and code, like you actually do, unless the server also sends valid non-error responses inside a error 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.

Community
  • 1
  • 1
Arseni Mourzenko
  • 50,338
  • 35
  • 112
  • 199
1

Add an Error property to your class, or even better use a base class with this error property, like this:

public class BaseResult
{
    public Error Error { get; set; }
    public bool HasError => String.IsNullOrEmpty(Error?.Code);
}

public class Error
{
    public string Status { get; set; }
    public string Code { get; set; }
}

Any result class inherits from this base result:

public class MyOkResponseClass : BaseResult
{
    public string Prop1 { get; set; }
    public string Prop2 { get; set; }
    public int Prop3 { get; set; }
}

Then you can check the property HasError. No exceptions, no extended methods and no weird checks.

Antonio Rodríguez
  • 976
  • 2
  • 11
  • 25
-3

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 :

// Invalid JSON
var responseContent = "asgdg"; 
// var responseContent = "{ \"ip\" = \"11.161.195.10\" }";

// Valid JSON, uncomment to test these
// var responseContent = "{ \"ip\": \"11.161.195.10\", \"city\": \"York\",  \"region\": \"Ontartio\",  \"country\": \"IN\",  \"loc\": \"-43.7334,79.3329\",  \"postal\": \"M1C\",  \"org\": \"AS577 Bell Afgh\",  \"readme\": \"https://ipinfo.io/missingauth\"}";
// var responseContent = "\"asfasf\"";
// var responseContent = "{}";

int count = 0;
foreach (char c in responseContent)
    if (c == '\"') count++; // Escape character needed to display quotation
if (count >= 2 || responseContent == "{}") 
{
    // Valid Json
    try {
        JToken parsedJson = JToken.Parse(responseContent);
        Console.WriteLine("RESPONSE: Json- " + parsedJson.ToString(Formatting.Indented));
    }  
    catch(Exception ex){
        Console.WriteLine("RESPONSE: InvalidJson- " + responseContent);
    }
}
else
    Console.WriteLine("RESPONSE: InvalidJson- " + responseContent);
Saamer
  • 4,687
  • 1
  • 13
  • 55
  • foreach (char c in responseContent) { if (c == '\"') count++; // Escape character needed to display quotation if (count > 1) break; } Although it takes more time to compile and run this, it would probably save time for very long JSON responses if you used this foreach – Saamer Aug 06 '19 at 17:33
  • 1
    "{}" is valid JSON, but is rejected but this code. "{ \"ip\" = \"11.161.195.10\" }" is not valid JSON and will throw an exception. – garethm Mar 25 '20 at 02:09