← Back to overview

FiveWin JSON Reader

Flatten any JSON to PATH / TYPE / VALUE rows and inspect with XBROWSER. Includes a tolerant JSON loader (BOM/^Z, optional double-decode).

POC FiveWin Harbour

What it does

Recursively walks a JSON tree, flattens it into rows (PATH, TYPE, VALUE), sorts by PATH, and shows the result in XBROWSER(). The loader strips UTF-8 BOM and trailing control characters (incl. 0x1A/^Z) and can handle a JSON string that itself contains JSON (double-encoded).

Usage

  • Adjust the path: c:\fwh\samples\sample.json (or change cJsonFile).
  • Compile with your regular FWH build (e.g. buildh.bat / hbmk2).
  • Run the EXE → an XBROWSER window opens with the flattened rows.

Features

  • Recursive flattening to PATH / TYPE / VALUE
  • XBROWSER table view
  • UTF-8 BOM / trailing control-char tolerant
  • Optional double-decode for JSON-in-JSON strings
  • Copy-ready source below

JsonFlattenBrowse.prg

/* JsonFlattenBrowse.prg – JSON rekursiv in 2D-Array für XBROWSER (FiveWin) */

#include "fivewin.ch"
#include "fileio.ch"

PROCEDURE Main()
   LOCAL cJsonFile := "c:\fwh\samples\sample.json"
   LOCAL uRoot, aRows := {}

   IF ! LoadJsonTolerant( cJsonFile, @uRoot )
      MsgStop( "JSON decode failed: " + cJsonFile )
      RETURN
   ENDIF

   // Rekursiv auflösen → aRows füllen { PATH, TYPE, VALUE }
   JsonFlattenToRows( uRoot, "", @aRows )

   // Optional: sortieren nach PATH
   ASort( aRows, , , {|a,b| a[1] < b[1] } )

   // Anzeigen (FWH helper)
   XBROWSER( aRows )   // Hinweis: in FWH heißt der Helper üblicherweise XBROWSER, nicht XBROWSE

   InKey(0)
RETURN

/* Rekursiv: füllt aRows mit { cPath, cType, cValue } */
FUNCTION JsonFlattenToRows( x, cPath, aRows )
   LOCAL i, cKey, cNextPath

   DO CASE
   CASE ValType( x ) == "H"                   // Hash/Object
      FOR EACH cKey IN hb_HKeys( x )
         cNextPath := IIF( Empty( cPath ), cKey, cPath + "." + cKey )
         JsonFlattenToRows( x[ cKey ], cNextPath, aRows )
      NEXT

   CASE ValType( x ) == "A"                   // Array
      FOR i := 1 TO Len( x )
         cNextPath := cPath + "[" + LTrim(Str(i)) + "]"
         JsonFlattenToRows( x[i], cNextPath, aRows )
      NEXT

   OTHERWISE                                  // Skalar
      AAdd( aRows, { cPath, ValType( x ), JF_ToString( x ) } )
   ENDCASE
RETURN NIL

/* Werte hübsch in String wandeln (Skalare); Strukturen kompakt als JSON */
FUNCTION JF_ToString( v )
   LOCAL t := ValType( v )
   DO CASE
   CASE t == "C" ; RETURN v
   CASE t == "N" ; RETURN hb_ntos( v )
   CASE t == "D" ; RETURN DToC( v )
   CASE t == "L" ; RETURN IIF( v, ".T.", ".F." )
   CASE t == "U" ; RETURN ""
   CASE t $ "HA" ; RETURN hb_jsonEncode( v )   // falls hier doch Struktur ankommt
   OTHERWISE     ; RETURN hb_ValToExp( v )
   ENDCASE
RETURN ""

/* ---- Robust JSON load: handles BOM, trailing control chars, and differing
       hb_jsonDecode() return conventions (logical OR numeric). ---- */

FUNCTION LoadJsonTolerant( cFile, uOut )
   LOCAL c := FileReadRaw( cFile )
   LOCAL cInner, vRet

   IF Empty( c )
      RETURN .F.
   ENDIF

   c := StripBom( c )     // remove EF BB BF if present
   c := RTrimCtl( c )     // strip trailing control chars (incl. 0x1A/^Z, CR/LF, NUL, DEL)

   /* 1) normal decode */
   vRet := hb_jsonDecode( c, @uOut )
   IF __JsonOk( vRet, uOut, c )
      RETURN .T.
   ENDIF

   /* 2) double-encoded case: file contains a JSON *string* which itself is JSON */
   cInner := NIL
   vRet := hb_jsonDecode( c, @cInner )
   IF __JsonOk( vRet, cInner, c ) .AND. HB_ISCHAR( cInner )
      vRet := hb_jsonDecode( cInner, @uOut )
      IF __JsonOk( vRet, uOut, cInner )
         RETURN .T.
      ENDIF
   ENDIF

RETURN .F.

/* Decide “success” across Harbour variants:
   - logical TRUE               → ok
   - numeric > 0 (bytes parsed) → ok
   - uVal got filled (some builds return NIL but fill @uVal) → ok
   - literal "null" input       → ok (maps to NIL) */
STATIC FUNCTION __JsonOk( vRet, uVal, cIn )
   LOCAL lOk := .F.
   IF HB_ISLOGICAL( vRet )
      lOk := vRet
   ELSEIF HB_ISNUMERIC( vRet )
      lOk := ( vRet > 0 )
   ELSEIF !HB_ISNIL( uVal )
      lOk := .T.
   ELSEIF Upper( AllTrim( cIn ) ) == "NULL"
      lOk := .T.
   ENDIF
RETURN lOk

/* --- helpers from your existing file (unchanged) --- */
FUNCTION FileReadRaw( cFile )
   LOCAL nH := FOpen( cFile, FO_READ ), c := "", nSize
   IF nH < 0
      RETURN c
   ENDIF
   nSize := FSeek( nH, 0, FS_END )
   FSeek( nH, 0, FS_SET )
   IF nSize > 0
      c := Space( nSize )
      FRead( nH, @c, nSize )
   ENDIF
   FClose( nH )
RETURN c

FUNCTION StripBom( c )
   IF Len( c ) >= 3 .AND. SubStr( c, 1, 3 ) == Chr(239)+Chr(187)+Chr(191)
      RETURN SubStr( c, 4 )
   ENDIF
RETURN c

FUNCTION RTrimCtl( c )
   LOCAL n := Len( c ), k
   DO WHILE n > 0
      k := Asc( SubStr( c, n, 1 ) )
      IF ( k >= 0 .AND. k <= 31 ) .OR. k == 127
         n--
      ELSE
         EXIT
      ENDIF
   ENDDO
   IF n < Len( c )
      RETURN Left( c, n )
   ENDIF
RETURN c

sample.json

{
  "Mensaje": {
    "Body": {
      "Pedidos": {
        "Detalles": [
          { "pedaux": 1, "pedcpr": "0007019", "pedctd": 12 },
          { "pedaux": 2, "pedcpr": "0007020", "pedctd": 8 }
        ]
      }
    }
  }
}