0

I am trying to build common function for JSON to CSV Converter. currently for different json file i have to make some changes in existing function.

current code:

function JsonToCSV($jfilename, $cfilename) {
        if (($json = file_get_contents($jfilename)) == false)
            die('Error reading json file...');
        $data = json_decode($json, true);
        $fp = fopen($cfilename, 'w');
        $header = false;
        foreach ($data as $row) {
            if (empty($header)) {
                $header = array_keys($row);
                fputcsv($fp, $header);
                $header = array_flip($header);
            }
            fputcsv($fp, array_merge($header, $row));
        }
        fclose($fp);
        return;
    }

Above code is working for below json

[
    {
        "Id": "1",
        "Name": "Juned Ansari",
        "Position": "Full Stack Developer",
        "Salary": "$99999"
    },
    {
        "Id": "2",
        "Name": "Mayur Marolia",
        "Position": "Data Analyst",
        "Salary": "$6789000"
    },
    {
        "Id": "3",
        "Name": "Mitesh Panchal",
        "Position": "Team Leader",
        "Salary": "$2324540"
    }
]

but the problem is if my json structure changed then i have to rewrite above function like it is not working for below json

[
    {
        "BILLING_SOFTWARE_API_KEY": "ABCD1234",
        "DISTRIBUTOR_API_KEY": "11380",
        "SALESMANS": [
            {
                "sm_code": 1,
                "sm_name": "DEEPAK MEHTA 7044524144"
            },
            {
                "sm_code": 2,
                "sm_name": "Juned Ansari"
            }
        ]
    }
]
Juned Ansari
  • 5,035
  • 7
  • 56
  • 89

2 Answers2

1

The problem is that JSON is unstructured, while CSV is structured.

To clear this hurdle you must first of all gather all the JSON fields in all the structure, and since the header must be written first, you need to cycle through the JSON twice.

$columns = [ ];
// This could be a foreach
// foreach($data as $row) { $columns = array_merge($columns, array_keys($row)); }
array_map(function($row) use (&$columns) {
    $columns = array_unique(array_merge($columns, array_keys($row)));
}, $data);
// Now columns contain all columns in all rows of the JSON.

$fp = fopen($cfilename, 'w');
fputcsv($fp, $columns);

// Set all empty fields to some default
$template = array_fill_keys($columns, '');

foreach ($data as $row) {
    fputcsv($fp, array_values(array_merge($template, $row)));
}
fclose($fp);

The above will not function out of the box for complex data (if a column has sub-information, like in your example). There you need a more complex step:

foreach ($data as $row) {
    $collapsed = array_map(function($value) {
        if (is_array($value)) {
            return implode(', ', $value);
        }
        return $value;
    }, $row);
    fputcsv($fp, array_merge($template, $collapsed));
}

Still more complex information in the JSON is a clear indication that you're doing this wrong. Your best bet is then to re-encode the complex value as JSON and store it as is in the CSV field (use json_encode() instead of implode, above).

The Great Column Name Massacre

For those cases where you need to throw bad money after worse, you can implement what I called the Great Column Name Massacre. In its easiest form, you code

{
    "address": {
        "street": "Piazza Vieusseux",
        "number": 2,
        "locality"  : {
             "type": "city",
             "name": "Florence"
        }
    }
}

as

[
    "address_street"          => "Piazza Vieusseux",
    "address_number"          => 2,
    "address_locality_type"   => "city",
    "address_locality_name"   => "Florence"
]

I'm feeling of two minds about this. Please do not take this wrong, but I'm feeling sort of like you asked me why your Smith&Wesson battery-operated hair dryer is not working, even if you put all six batteries in the drum, pointed it to your head and pulled the trigger.

And I feel like I'm telling you "Oh, there's a safety switch on the side. You need to move it from SAFE to FIRE, or it will not work".

So bearing in mind that this looks like a very bad idea, the folding function I mentioned in the comments is this (you can adapt it to your needs, see later):

function fold($arry, $prefix = '') {
    $retval = [ ];
    foreach ($arry as $key => $value) {
        $newkey = $prefix.$key;
        if (is_array($value)) {
            $folded = fold($value, $newkey . '_');
            foreach ($folded as $subkey => $subval) {
                $retval[$subkey] = $subval;
            }
        } else {
            $retval[$newkey] = $value;
        }
    }
    return $retval;
}

Once each element of the array has been folded, it can be analyzed to find out the column names (you can do this while folding) and then everything proceeds like above.

Testing

The folding function works properly with the provided JSON sample, and yields

Array
(
    [BILLING_SOFTWARE_API_KEY] => ABCD1234
    [DISTRIBUTOR_API_KEY] => 11380
    [SALESMANS_0_sm_code] => 1
    [SALESMANS_0_sm_name] => DEEPAK MEHTA 7044524144
    [SALESMANS_1_sm_code] => 2
    [SALESMANS_1_sm_name] => Juned Ansari
)

This of course immediately raises the first problem; "DISTRIBUTOR_API_KEY" is what we would expect from {"DISTRIBUTOR": {"API": {"KEY": 11380}}}. It works, but the decoding is ambiguous.

To overcome this limitation the quickest way is to change the delimiter from '_' to something else, or encode it differently in the keys.

Be warned: there will be no end of problems with this approach. If I had the time, I flatter myself fantasizing that I might end up with a post to rival the Famous Answer.

Having not the time, I hereby decline all responsibility for the frightful damage, loss of property, loss of productivity, loss of time, dereliction of duty, alienation of spouse and family ties and dire health consequences (included, but not limited to, a painful and prolonged death) that I believe are sure to follow.

LSerni
  • 55,617
  • 10
  • 65
  • 107
  • json can be in any format, above 2 json is not fixed in future multi dimensional json can also come – Juned Ansari Nov 08 '17 at 13:19
  • 1
    Yes. But the question is: how does the CSV handle this? Not only this might depend on the field structure, *it could be different for every row*. You will never find a "universal function" (unless you go with the json_encode solution, or maybe with the Great Column Name Massacre (which not all CSV parsers accept)). At that point, it **is** better and much more maintainable to rewrite the encoder every time, to better drive home that you're dealing with a completely different object. – LSerni Nov 08 '17 at 13:34
  • 1
    Yes. That's the *Great Column Name Massacre*, where you first code all the paths inside the whole JSON into strings, so that you end up with an array of keypairs (whose name might be longer than your CSV parser will support. Then you'll need *column name compression*. Then you'll end storing the compressed structure of each field name in the fields of the very first row of the CSV. Yeah, BTDTGTTS). And you know what? It's *still* not (*straightforwardly!*) compatible with all JSON. `[{"a": [{"b":"c"},{"d":"e"}]},{"b":"x"}]` will code three lines instead of two. – LSerni Nov 08 '17 at 14:01
0

I use simple client SIDE to convert JSON/HTML/XML to CSV,EXCEL...

because it is easy to download by attaching the file to download attribute of anchor tag...

here is an example you may like ...

The JS FIDDLE

$(document).ready(function(){
    $('button').click(function(){
        var data = $('#txt').val();
        if(data == '')
            return;

        JSONToCSVConvertor(data, "Vehicle Report", true);
    });
});

function JSONToCSVConvertor(JSONData, ReportTitle, ShowLabel) {
    //If JSONData is not an object then JSON.parse will parse the JSON string in an Object
    var arrData = typeof JSONData != 'object' ? JSON.parse(JSONData) : JSONData;

    var CSV = '';    
    //Set Report title in first row or line

    CSV += ReportTitle + '\r\n\n';

    //This condition will generate the Label/Header
    if (ShowLabel) {
        var row = "";

        //This loop will extract the label from 1st index of on array
        for (var index in arrData[0]) {

            //Now convert each value to string and comma-seprated
            row += index + ',';
        }

        row = row.slice(0, -1);

        //append Label row with line break
        CSV += row + '\r\n';
    }

    //1st loop is to extract each row
    for (var i = 0; i < arrData.length; i++) {
        var row = "";

        //2nd loop will extract each column and convert it in string comma-seprated
        for (var index in arrData[i]) {
            row += '"' + arrData[i][index] + '",';
        }

        row.slice(0, row.length - 1);

        //add a line break after each row
        CSV += row + '\r\n';
    }

    if (CSV == '') {        
        alert("Invalid data");
        return;
    }   

    //Generate a file name
    var fileName = "MyReport_";
    //this will remove the blank-spaces from the title and replace it with an underscore
    fileName += ReportTitle.replace(/ /g,"_");   

    //Initialize file format you want csv or xls
    var uri = 'data:text/csv;charset=utf-8,' + escape(CSV);

    // Now the little tricky part.
    // you can use either>> window.open(uri);
    // but this will not work in some browsers
    // or you will not get the correct file extension    

    //this trick will generate a temp <a /> tag
    var link = document.createElement("a");    
    link.href = uri;

    //set the visibility hidden so it will not effect on your web-layout
    link.style = "visibility:hidden";
    link.download = fileName + ".csv";

    //this part will append the anchor tag and remove it after automatic click
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}
Rohit Kumar
  • 1,777
  • 2
  • 13
  • 26