Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Delphi JSON richtig parsen (https://www.delphipraxis.net/192153-json-richtig-parsen.html)

Hobbycoder 23. Mär 2017 10:17

JSON richtig parsen
 
Hi,

ich versucht seit Stunden ein JSON-String zu parsen, aber ich raff's nicht. Ich könnte mal ein bisschen Schützenhilfe von euch gebrauchen ;-)

Mein JSON-String sieht so aus:
Code:
[{"Lfdnr":"1","Fehlernummer":"1","Programmversion":"4.6.1.123","DBVersion":"15","Status":"1","Kategorie":"Testfehler","Dringlichkeit":"1","NoWork":"0","Kurzbeschreibung":"Das ist ein Testfehler","Langbeschreibung":"Das ist der Langtext von Testfehler","Benutzer":"HC","BenutzerName":"Hobbycoder","Datum":"2017-03-22 00:00:00","BehInVersion":"","BehDatum":"0000-00-00 00:00:00","BehVon":""},{"Lfdnr":"2","Fehlernummer":"1","Programmversion":"4.6.1.123","DBVersion":"15","Status":"1","Kategorie":"Testfehler","Dringlichkeit":"1","NoWork":"0","Kurzbeschreibung":"Das ist ein Testfehler","Langbeschreibung":"Das ist der Langtext von Testfehler","Benutzer":"HC","BenutzerName":"Hobbycoder","Datum":"2017-03-22 00:00:00","BehInVersion":"","BehDatum":"0000-00-00 00:00:00","BehVon":""},{"Lfdnr":"3","Fehlernummer":"2","Programmversion":"4.6.1.155","DBVersion":"200","Status":"2","Kategorie":"Nocheinekategorie","Dringlichkeit":"3","NoWork":"1","Kurzbeschreibung":"kb","Langbeschreibung":"lb","Benutzer":"WT","BenutzerName":"Willi Tester","Datum":"2017-03-22 00:00:00","BehInVersion":"","BehDatum":"0000-00-00 00:00:00","BehVon":""}]
Ziel ist es alle Elemente des Array in Objekte zu überführen.
Die Objekte sehen (etwas gekürzt) so aus:
Delphi-Quellcode:
 TErrorMessage=class
    property Lfdnr: Integer read FLfdnr write SetLfdnr;
    property Fehlernummer: string read FFehlernummer write SetFehlernummer;
    property Programmversion: string read FProgrammversion write SetProgrammversion;
    property DBVersion: string read FDBVersion write SetDBVersion;
    property Status: TEMStatus read FStatus write SetStatus;
    property Kategorie: string read FKategorie write SetKategorie;
    property Dringlichkeit: TEMDringlichkeit read FDringlichkeit write SetDringlichkeit;
    property NoWork: Boolean read FNoWork write SetNoWork;
    property Kurzbeschreibung: string read FKurzbeschreibung write SetKurzbeschreibung;
    property Langbeschreibung: string read FLangbeschreibung write SetLangbeschreibung;
    property Benutzer: string read FBenutzer write SetBenutzer;
    property Benutzername: string read FBenutzername write SetBenutzername;
    property Datum: TDateTime read FDatum write SetDatum;
    property BehInVersion: string read FBehInVersion write SetBehInVersion;
    property BenDatum: TDateTime read FBenDatum write SetBenDatum;
    property BenVon: string read FBenVon write SetBenVon;
  end;
Das Objekt habe ich hier nur mal aufgeführt, damit man sehen kann wie ich das ablegen will.

Mein Source dafür sie so aus:
Delphi-Quellcode:
procedure TErrorMessageList.GetListFromWeb(path: string);
var
  http: TIdHTTP;
  jsonstr: string;
  st: TStringStream;
  jsonval: TJSONValue;
  jsondsz: TJSONValue;
  em: TErrorMessage;
begin
  http:=TIdHTTP.Create(nil);
  st:=TStringStream.Create;
  try
    self.Clear;
    http.Get(path+'getlist.php', st);
    st.Position:=0;
    jsonstr:=st.ReadString(st.Size);
    try
      jsonval:=TJSONObject.ParseJSONValue(jsonstr);
//      if json is TJSONArray then
//      begin
        for jsondsz in (jsonval as TJSONArray) do
        begin
          Try
            em:=TErrorMessage.Create;
            em.Lfdnr:=StrToInt(TJSONString(jsondsz.GetValue('lfdnr')).Value);
            em.Fehlernummer:=jsondsz.GetValue('fehlernummer').ToString;
            em.Programmversion:=jsondsz.GetValue('programmversion').ToString;
            em.DBVersion:=jsondsz.GetValue('dbversion').ToString;
            em.Status:=TEMStatus(StrToInt(jsondsz.GetValue('status').ToString));
            em.Kategorie:=jsondsz.GetValue('kategorie').ToString;
            em.Dringlichkeit:=TEMDringlichkeit(StrToInt(jsondsz.GetValue('dringlichkeit').ToString));
            em.NoWork:=Int2Bool(StrToInt(jsondsz.GetValue('nowork').ToString));
            em.Kurzbeschreibung:=jsondsz.GetValue('kurzbeschreibung').ToString;
            em.Langbeschreibung:=jsondsz.GetValue('langbeschreibung').ToString;
            em.Benutzer:=jsondsz.GetValue('benutzer').ToString;
            em.Benutzername:=jsondsz.GetValue('benutzername').ToString;
            self.Add(em);
          except
            em.Free;
          End;
        end;
      //end;
    finally
    end;
  finally
    http.Free;
    st.Free;
  end;
end;
Nur so, läuft er nicht. Sieht auch ein bisschen verrissen aus, weil ich auf Grund der Beispiele im Internet das eine oder andere ausprobiert habe, aber nicht wirklich zu einem lauffähigen Ergebnis gekommen bin.
Nach Möglichkeit will ich nur System.json verwenden, und keine anderen oder fremde Units.

Wäre für eure Hilfe sehr dankbar.

Gruß Hobbycoder.

Der schöne Günther 23. Mär 2017 10:32

AW: JSON richtig parsen
 
Liste der Anhänge anzeigen (Anzahl: 1)
Du machst es dir viel zu kompliziert - Das ist alles schon fertig: Mit TJson.JsonToObject<TErrorMessage>(yourJsonObject) hast du eine automatische Umwandlung deines Json-Objekts in TErrorMessage.

Hier als Beispiel:

Delphi-Quellcode:
// JCL_DEBUG_EXPERT_GENERATEJDBG OFF
// JCL_DEBUG_EXPERT_INSERTJDBG OFF
// JCL_DEBUG_EXPERT_DELETEMAPFILE OFF
program Project19;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.IoUtils,
  System.Json,
  Rest.Json,
  Unit10 in 'Unit10.pas';


const
   filePath = 'y:\jsonTest.txt';

procedure parseJson();
var
   fileContent:       String;
   messagesJson:      TJSONArray;
   messageJson:      TJSONValue;

   messageObject:      TErrorMessage;
   messagesObjects:   TArray<TErrorMessage>;
begin
   fileContent := TFile.ReadAllText(filePath);

   messagesJson := TJSONObject.ParseJSONValue(fileContent) as TJSONArray;

   messagesObjects := [];
   for messageJson in messagesJson do begin
      messageObject := TJson.JsonToObject<TErrorMessage>(messageJson as TJsonObject);
      messagesObjects := messagesObjects + [messageObject];
   end;

   readln;
end;

begin
   parseJson();
end.

PS: Ja, die Umwandlung stammt aus Rest.Json.pas die beim RAD-Studio dabei ist. Ich wundere mich auch, was das mit REST zu tun haben soll, warum das nicht auch in System.Json.pas steckt. Aber das ist hoffentlich kein Stolperstrick?

Hobbycoder 23. Mär 2017 11:55

AW: JSON richtig parsen
 
Danke ;-) Das funktioniert ja wie geschnitten Brot ;-)

Zu meinem Verständnis: Die Voraussetzungen sind, nehme ich mal an, dass a) der JSON-String NUR ein Array enthält, und b) die Bezeichner für die Values innerhalb des Array den Properties des Objekts entsprechen?
Wie findet die Funktion denn die passenden Properties? Und werden die Typen immer passend umgewandelt (String, Int, TDatetime, etc)?

Der schöne Günther 23. Mär 2017 12:03

AW: JSON richtig parsen
 
Zu a)

Ja - Eigentlich musst du ja im Voraus wissen, was du bekommst. Ob es ein Array mit X Dingen drin ist, oder nur ein Ding. Wenn beides auftreten kann muss dein Code mit beiden Wegen zurechtkommen. Oder meinst du dass der "richtige" Inhalt wiederum ein Objekt mit einer Handvoll Feldern und ein Feld ist dieses Array? Auch das ist kein Problem, man muss es nur abbilden im Code.


Zu b)
Das Umwandeln des Json in dein TErrorMessage-Objekt fasst die Properties nicht an. Das siehst du wenn du z.B. einen Haltepunkt in deine Setter-Methode setzt. Der "Marshaller" erstellt zuerst ein TErrorMessage-Objekt, dann sucht er sich für jedes JSON-Feld das passende Feld in deinem Delphi-Objekt und steckt den Wert da hinein. Da der JSON-String den du angegeben hast anscheinend einwandfrei war, klappt das auch mit Dingen wie TDateTime (wie man sieht).

Eine Besonderheit: In deinem Fall hat das so toll geklappt weil du dieser merkwürdigen Konvention (bzw. oft leider Notwendigkeit) folgst allen Feldnamen immer ein "F" voran zu stellen. In deinem Json-String ist das nicht so. Trotzdem hat das geklappt. Warum das so ist steht hier:
http://www.delphipraxis.net/182105-f...unmarshal.html

Ich rege mich darüber eher auf, aber im verlinkten Thema steht auch wie du z.B. die Felder in deiner Klasse auch anders nennen kannst als im JSON und der Mechanismus trotzdem weiterhin funktionieren kann.

Hobbycoder 23. Mär 2017 12:33

AW: JSON richtig parsen
 
Den verlinkten Artikel habe ich gelesen.

Demnach scheint, auch wenn du keine Freund davon bist, eine einheitliche Namenskonvention zwischen json und objekt für die geringsten Komplikationen zu sorgen. Damit kann ich leben.
Mein Objekt umfasst ja mehr Properties, als im Json übermittelt. Du Funktion hat damit keine Probleme.
Was aber passiert im umgekehrten Fall? Kommt eine Exception, oder ignoriert die Funktion das dann einfach? Das könnte ja schnell passieren, denn nicht immer hat man selbst Einfluss auf den JSON-String bzw. wann der mal verändert wird.
(Ich könnt's auch ausprobieren, aber vielleicht weißt du das auch so.)

Hobbycoder 23. Mär 2017 12:42

AW: JSON richtig parsen
 
Und ein Problem hab ich dann doch noch.
Ich habe ja 2 Properties als enumeration type.
Delphi-Quellcode:
  TEMStatus=(emsNeu, emsIB, emsKorr, emsGepr);
  TEMDringlichkeit=(emdLowest, emdLower, emdNormal, emdHigh, emdHighest);

  property Status: TEMStatus read FStatus write SetStatus;
  property Dringlichkeit: TEMDringlichkeit read FDringlichkeit write SetDringlichkeit;
Das würde ich auch gerne so belassen.

Diese werden von TJson.JsonToObject<TErrorMessage>(messageJson as TJsonObject) nicht korrect gesetzt. Wahrscheinlich eher gar nicht, da json selbst diese Datentypen nicht kennt.
Da ich sie auch nicht im Create vordefiniere, bleiben sie ohne wert und können bei der späteren Verwendung auch nicht korrekt gecastet werden.

Wie löst man sowas? Im JSON sind die ja als Integer drin.

Der schöne Günther 23. Mär 2017 17:54

AW: JSON richtig parsen
 
Zu 1)
Ich habe es nicht im Kopf, aber durch Ausprobieren sieht man meine ich dass er weder auf die Nase fällt wenn dein Objekt mehr Felder hat als das Json, noch umgekehrt wenn das Json mehr Felder hat als dein Objekt. Eigentlich ist dir ja klar was du als Json bekommst. Das ist doch ein perfekter Kandidat für Unit-Tests. Sollte sich der Json-Mechanismus in Delphi eines Tages ändern würdest du das mit deinen Tests ja sofort merken wenn dort etwas nicht mehr stimmt ;-)

Zu 2)
Enumemerationen sind jetzt etwas kniffelig. Denn in JSON gibt es so etwas nicht. Deine Gegenstelle legt die jetzt als Integer ab sagst du. Hätte man auch als Strings machen können. Ich sehe zwei Möglichkeiten:
  1. Einen eigenen JSON-Interceptor bauen
  2. Die billige Lösung

Der Interceptor würde die Zahl aus deinem Json auswerten können und wieder auf deinen Enum-Typen zurückführen können. Ich finde hier im Forum einmal etwas dazu, bekomme es selbst aber auf die Schnelle nicht mehr hin.

Ich finde es auch in dem Fall einen Overkill. Im Fall Enum/Zahl - ganz ehrlich - Warum nicht einfach die Felder in deinem Objekt auch als Zahl (z.B. Byte) machen und die Getter & Property anpassen? Zu irgend etwas müssen diese Getter ja gut sein ;-)


Delphi-Quellcode:
type
   TEMStatus=(emsNeu, emsIB, emsKorr, emsGepr);

   TEMDringlichkeit=(emdLowest, emdLower, emdNormal, emdHigh, emdHighest);

   TErrorMessage = class
      private var
         FStatus:      Byte;
         FDringlichkeit:   Byte;
      private
         function getStatus(): TEMStatus;
         function getDringlicheit: TEMDringlichkeit;
      public
         property Status: TEMStatus read getStatus;
         property Dringlichkeit: TEMDringlichkeit read getDringlicheit;
   end;


implementation

{ TErrorMessage }

function TErrorMessage.getDringlicheit(): TEMDringlichkeit;
begin
   Result := TEMDringlichkeit(FDringlichkeit);
end;

function TErrorMessage.getStatus(): TEMStatus;
begin
   Result := TEMStatus(FStatus);
end;

Jumpy 24. Mär 2017 08:02

AW: JSON richtig parsen
 
Warum kann man eigentlich Properties nicht überladen? Das wäre doch jetzt echt praktisch für diesen Fall. :-D

DerDan 24. Mär 2017 11:06

AW: JSON richtig parsen
 
Properties kann man zwar nicht überladen, die Getter und Setter aber schon, fals diese in der Basisklasse entsprechend angelegt wurden

Hobbycoder 25. Mär 2017 00:07

AW: JSON richtig parsen
 
Ich habe das mit den enumererationen jetzt so gelöst, in dem ich mir eine Helperklasse angelegt habe, in die der Json geparst wird. Danach überführe ich das in die Zielklasse mit entsprechenden Umsetzungen. Nicht die schönste Lösung, aber für meine Zwecke reichst.


Alle Zeitangaben in WEZ +1. Es ist jetzt 23:49 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz