Deserialisieren Sie json auf "TryParse" Weise

76

Wenn ich eine Anfrage an einen Dienst sende (den ich nicht besitze), antwortet dieser möglicherweise entweder mit den angeforderten JSON-Daten oder mit einem Fehler, der wie folgt aussieht:

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

In beiden Fällen ist der HTTP-Antwortcode 200 OK, daher kann ich damit nicht feststellen, ob ein Fehler vorliegt oder nicht. Ich muss die Antwort deserialisieren, um sie zu überprüfen. Ich habe also etwas, das so aussieht:

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

Hier habe ich eine leere catch-Klausel, die sich möglicherweise im Standardausführungspfad befindet, was ein schlechter Geruch ist ... Nun, mehr als ein schlechter Geruch: Es stinkt.

Kennen Sie einen besseren Weg, um die Antwort "TryParse" zu machen, um einen Haken im Standardausführungspfad zu vermeiden ?

[BEARBEITEN]

Dank der Antwort von Yuval Itzchakov habe ich meine Methode folgendermaßen verbessert:

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

Ich habe die catch-Klausel beibehalten ... nur für den Fall.

Alter Pascalou
quelle

Antworten:

56

Mit können Json.NETSie Ihren JSON anhand eines Schemas validieren:

 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
}

Und verwenden Sie das dann in einer TryParse-Methode.

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

Dann mach:

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

Aktualisieren:

Beachten Sie, dass die Schemaüberprüfung nicht mehr Teil des Hauptpakets Newtonsoft.Json ist. Sie müssen das Paket Newtonsoft.Json.Schema hinzufügen .

Update 2:

Wie in den Kommentaren erwähnt, hat "JSONSchema" ein Preismodell, was bedeutet, dass es nicht kostenlos ist . Alle Informationen finden Sie hier

Yuval Itzchakov
quelle
12
Dieses Paket erfordert auch eine Lizenz, um mehr als 1000 Validierungen pro Stunde zu verwenden: newtonsoft.com/jsonschema
dpix
@dpix Auf jeden Fall wichtig zu beachten. Ich werde das der Antwort hinzufügen, danke.
Yuval Itzchakov
In meinem Fall benutze ich github.com/RicoSuter/NJsonSchema
Mario Villanueva
42

Die Antwort von @Victor LG mit Newtonsoft ist nah, aber es vermeidet technisch nicht den Haken, den das Originalplakat angefordert hat. Es bewegt es einfach woanders hin. Obwohl eine Einstellungsinstanz erstellt wird, mit der fehlende Mitglieder abgefangen werden können, werden diese Einstellungen nicht an den DeserializeObject-Aufruf übergeben, sodass sie tatsächlich ignoriert werden.

Hier ist eine "catch free" -Version seiner Erweiterungsmethode, die auch das Flag für fehlende Mitglieder enthält. Der Schlüssel zum Vermeiden des Fangs besteht darin, die ErrorEigenschaft des Einstellungsobjekts auf ein Lambda zu setzen, das dann ein Flag setzt, um einen Fehler anzuzeigen, und den Fehler löscht, damit keine Ausnahme verursacht wird.

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

Hier ist ein Beispiel, um es zu verwenden:

if(value.TryParseJson(out MyType result))
{ 
    // Do something with result…
}
Steve In CO
quelle
3
Beachten Sie, dass dies für leere Objekte (z. B. "{}") true zurückgibt, es sei denn, Sie verwenden [JsonProperty (Required = Required.Always)] für Ihre Ergebnisklasse.
user764754
@ user764754 Dies gilt jedoch nicht nur für diese Implementierung. Beim Aufrufen JsonConvert.DeserializeObject<T>("{}")wird keine Ausnahme ausgelöst, Tes sei denn, es handelt sich um einen Array-Typ (oder eine Eigenschaft ist erforderlich, wie Sie sagten).
Gabriel Luci
2
Technisch gesehen vermeidet diese Antwort auch nicht unbedingt den Haken. Wenn Sie ungültiges JSON an diese Methode übergeben, gibt JsonConvert.DeserializeObject einen Fehler aus und fängt ihn intern ab. In diesem Fall verschiebt diese Methode den Fang nur an eine andere Stelle.
Asthomas
29

Eine leicht modifizierte Version von @ Yuvals Antwort.

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

Dies kann verwendet werden, wenn das Schema als Text für keinen Typ verfügbar ist.

M22an
quelle
Neugierig, wenn dies: JsonConvert.DeserializeObject <T> (jsonData) ist besser als nur (T) jObject. Sie haben bereits einmal für JObject deserialisiert, also scheint eine Besetzung billiger zu sein? Ich weiß es wirklich nicht.
LösenJ
1
In vielen Fällen kann es auch ratsam sein, JObject.Parse () in eine separate Methode mit einem separaten try / catch zu verpacken, wenn Sie den JSON nicht steuern, da er eine Ausnahme für ungültiges JSON auslöst. Hier können zwei unterschiedliche Bedingungen auftreten: 1) Ungültiger JSON, 2) Json stimmt nicht mit dem erwarteten Schema überein. In unserem Fall gehen wir anders vor.
LösenJ
Meine erste Frage zum Deserialisieren und Casting ist ungültig. Hätte sagen sollen: jObject.ToObject <T> ()
LösenJ
@solvingJ, die Behandlung von ungültigem JSON und JSON, die nicht mit dem Schema übereinstimmen, kann separat behandelt werden. In unserem Fall erwarten wir jedoch kein ungültiges JSON und dies wird in einer anderen Ebene behandelt, sodass dies für uns sinnvoll ist. Diese JObject jObject = JObject.Parse(jsonData);Prüfung ist für unseren Fall überflüssig.
M22an
19

Nur um ein Beispiel für den Try / Catch-Ansatz zu geben (er kann für jemanden nützlich sein).

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

Dann kann es so verwendet werden:

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

if(isValidObject)
{
    // Do something
}
Victor LG
quelle
1
Das scheint nicht zu funktionieren. DeserializeObject löst keine Ausnahme aus, wenn es sich um einen gültigen JSON handelt, der nicht mit dem Objekt übereinstimmt. Die Verwendung dieser Methode zur Lösung der ursprünglichen Frage erfordert eine weitere Validierung.
Derrick
2
Sie haben vollkommen recht @Derrick, danke, dass Sie darauf hingewiesen haben. Ich habe gerade meine Antwort aktualisiert, um zu überprüfen, ob Felder des zu deserialisierenden Objekts fehlen. Hinweis: Credits des Updates basierend auf dieser Antwort: stackoverflow.com/questions/21030712/…
Victor LG
1
Ich habe immer noch einen Tippfehler in Ihrem Codebeispiel. Sollte sein result = JsonConvert.DeserializeObject<T>(obj, settings);-
Richard Moore
1
Wird auch in geändert catch (JsonSerializationException ex) , catch (Exception)sodass false zurückgegeben wird, wenn ungültiger Json an ihn übergeben wird.
Richard Moore
3

Sie können JSON zu a deserialisierendynamic und prüfen, ob das Stammelement vorhanden ist error. Beachten Sie, dass Sie wahrscheinlich nicht auf das Vorhandensein von statusund überprüfen müssen code, wie Sie es tatsächlich tun, es sei denn, der Server sendet auch gültige fehlerfreie Antworten innerhalb eines errorKnotens.

Abgesehen davon glaube ich nicht, dass Sie es besser machen können als a try/catch.

Was tatsächlich stinkt, ist, dass der Server ein HTTP 200 sendet, um einen Fehler anzuzeigen. try/catcherscheint einfach als Überprüfung der Eingänge.

Arseni Mourzenko
quelle
Danke für deine Antwort. Sie haben Recht: Was tatsächlich stinkt, ist der HTTP 200 OK-Code, um einen Fehler anzuzeigen ...
Dude Pascalou
-3

Um zu testen, ob ein Text unabhängig vom Schema ein gültiger JSON-Code ist, können Sie auch die Anzahl der Anführungszeichen überprüfen: "in Ihrer Zeichenfolgenantwort, wie unten gezeigt:

// 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
quelle
foreach (char c in responseContent) {if (c == '\ "') count ++; // Escape-Zeichen, das zum Anzeigen des Zitats benötigt wird, wenn (count> 1) break;} Obwohl das Kompilieren und Ausführen länger dauert, würde dies der Fall sein Sparen Sie wahrscheinlich Zeit für sehr lange JSON-Antworten, wenn Sie diese foreach
Saamer
"{}" ist gültiger JSON, wird aber mit diesem Code abgelehnt. "{" ip "=" 11.161.195.10 "}" ist kein gültiger JSON und löst eine Ausnahme aus.
Garethm