web-dev-qa-db-fra.com

Désérialiser json d'une manière "TryParse"

Lorsque j'envoie une demande à un service (que je ne possède pas), il peut répondre soit avec les données JSON demandées, soit avec une erreur ressemblant à ceci:

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

Dans les deux cas, le code de réponse HTTP est 200 OK. Je ne peux donc pas l'utiliser pour déterminer s'il existe une erreur ou non. Je dois désérialiser la réponse pour la vérifier. J'ai donc quelque chose qui ressemble à ceci:

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

Ici, j'ai une clause catch vide qui peut être dans le chemin d'exécution standard, ce qui est une mauvaise odeur ... Eh bien, plus qu'une mauvaise odeur: ça pue.

Connaissez-vous un meilleur moyen "TryParse" de répondre pour éviter une prise dans le chemin d'exécution standard?

[MODIFIER]

Grâce à la réponse de Yuval Itzchakov , j'ai amélioré ma méthode de la manière suivante:

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

J'ai gardé la clause de capture ... juste au cas où.

53
Dude Pascalou

Avec Json.NET vous pouvez valider votre json par rapport à un schéma:

 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
}

Et utilisez ensuite cela dans une méthode TryParse.

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

Alors fais:

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

Mise à jour:

Veuillez noter que la validation de schéma ne fait plus partie du package principal Newtonsoft.Json. Vous devez donc ajouter le package Newtonsoft.Json.Schema .

Mise à jour 2:

Comme indiqué dans les commentaires, "JSONSchema" a un modèle de tarification, ce qui signifie qu'il n'est pas gratuit . Vous pouvez trouver toutes les informations ici

49
Yuval Itzchakov

Une version légèrement modifiée de la réponse de @ Yuval.

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

Cela peut être utilisé lorsque le schéma n'est pas disponible sous forme de texte, quel que soit le type.

24
M22an

La réponse de @Victor LG en utilisant Newtonsoft est proche, mais techniquement, elle n’évite pas les pièges de l’affiche originale demandée. Cela le déplace simplement ailleurs. De plus, bien qu'il crée une instance de paramètres pour permettre la capture des membres manquants, ces paramètres ne sont pas transmis à l'appel DeserializeObject, ils sont donc ignorés.

Voici une version "catch free" de sa méthode d'extension qui inclut également l'indicateur de membres manquants. La clé pour éviter le piège consiste à définir la propriété Error de l'objet settings sur un lambda, qui définit ensuite un indicateur indiquant un échec et efface l'erreur afin d'éviter toute 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;
}

Voici un exemple pour l'utiliser:

if(value.TryParseJson(out MyType result))
{ 
    // Do something with result…
}
19
Steve In CO

Juste pour donner un exemple de l'approche try/catch (cela peut être utile à quelqu'un).

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

Ensuite, il peut être utilisé comme ceci:

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

if(isValidObject)
{
    // Do something
}
10
Victor LG

Vous pouvez désérialiser JSON sur un dynamic et vérifier si l'élément racine est error. Notez que vous n’aurez probablement pas à vérifier la présence de status et de code, comme vous le faites réellement, à moins que le serveur envoie également des réponses valables sans erreur à l’intérieur d’un error noeud.

À part cela, je ne pense pas que vous puissiez faire mieux qu'un try/catch.

Ce qui pue réellement, c'est que le serveur envoie un HTTP 200 pour indiquer une erreur. try/catch Apparaît simplement comme vérification des entrées.

3
Arseni Mourzenko