以“ TryParse”方式反序列化 json

当我向一个服务发送一个请求(我并不拥有)时,它可能会响应所请求的 JSON 数据,或者出现如下错误:

{
"error": {
"status": "error message",
"code": "999"
}
}

在这两种情况下,HTTP 响应代码都是200 OK,所以我不能用它来确定是否存在错误-我必须反序列化响应来检查。 所以我有这样的东西:

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;
}

在这里,我有一个可能位于标准执行路径中的空 catch 子句,这是一种不好的气味... ... 好吧,不仅仅是不好的气味: 它很臭。

你知道一个更好的方法来 “尝试分析”的响应,以 避免陷入困境在标准的执行路径?

[编辑]

感谢 Yuval Itzchakov的回答,我改进了我的方法:

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;
}
}

我保留了条款... 以防万一。

109248 次浏览

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.

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

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.

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
}

@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…
}

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);

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.