accessing parsed json data in perl - json

I have the following json data in a string $json:
{
"results": [{
"geometry": {
"location": {
"lat": 37.4224764,
"lng": -122.0842499
},
},
}]
}
I use the following to decode my JSON data.
my $decoded_json = decode_json( $json );
And I'm attempting at retrieving the latitude here:
my $lat=$decoded_json->{results}->{geometry}{location}{lat};
And Here:
my $lat=$decoded_json->{results}->{geometry}->{location}->{lat};
I'd like to know how to retrieve the lat and lng from this decoded data.

There's an array in your JSON, but no array dereference in your code. In other words, you completely overlooked the [ ... ] in the JSON. You need to specify which of the results you want. In the example in question, there is only one, so you could use the following:
my $loc = $decoded_json->{results}[0]{geometry}{location};
my $lat = $loc->{lat};
my $lng = $loc->{lng};
Of course, the whole point of using an array of results is that the number of results might vary. You will probably need a loop.
for my $result (#{ $decoded_json->{results} }){
my $loc = $result->{geometry}{location};
my $lat = $loc->{lat};
my $lng = $loc->{lng};
...
}
About the arrows...
my $lat = $decoded_json->{results}[0]{geometry}{location}{lat};
is short for
my $lat = $decoded_json->{results}->[0]->{geometry}->{location}->{lat};
When the -> is between {...} or [...], and {...} or [...], it can be omitted.
That's why there's no difference between
$result->{geometry}{location}{lat};
$result->{geometry}{location}->{lat};
( $result->{geometry}{location} )->{lat}; # Can't be omitted here.
my $loc = $result->{geometry}{location};
my $lat = $loc->{lat}; # Can't be omitted here.

Given the name of the key, "results", it is natural to expect there might be times where you get more than one result. The following will give you the coordinates for all results returned:
for my $r (#{ $decoded_json->{results} }) {
my ($lat, $lng) = #{ $r->{geometry}{location} }{qw(lat lng)}
# do something with coordinates
}
With recent Perl versions, you can re-write that as:
for my $r ($decoded_json->{results}->#*) {
my ($lat, $lng) = $r->{geometry}{location}->#{qw(lat lng)};
# do something with coordinates
}
Once again, decode_json gives you a Perl data structure. The fact that the contents of that structure were constructed by parsing a JSON document are completely irrelevant to what you do with it.
Working example (Cpanel::JSON::XS chokes on your JSON):
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use JSON::MaybeXS qw( decode_json );
my $json_string = <<EO_JSON;
{
"results": [{
"geometry": {
"location": {
"lat": 37.4224764,
"lng": -122.0842499
}
}
}]
}
EO_JSON
my $data = decode_json($json_string);
for my $r (#{ $data->{results} }) {
my ($lat, $lng) = #{ $r->{geometry}{location} }{qw(lat lng)};
print "($lat,$lng)\n";
}
for my $r ($data->{results}->#*) {
my ($lat, $lng) = $r->{geometry}{location}->#{qw(lat lng)};
print "($lat,$lng)\n";
}

Related

Getting individual key values from Perl JSON one at a time

I have JSON code that I'm pulling with key names that are the same and I'm trying to pull the values from the keys one at a time and pass them to variables (in a loop) in a perl script but it pulls all of the values at one time instead of iterating through them. I'd like to pull a value from a key and pass it to a variable then iterate through the loop again for the next value. The amount of data changes in JSON so the amount of identical keys will grow.
Perl Script Snippet
#!/usr/bin/perl
use warnings;
use strict;
use JSON::XS;
my $res = "test.json";
my $txt = do {
local $/;
open my $fh, "<", $res or die $!;
<$fh>;
};
my $json = decode_json($txt);
for my $mdata (#{ $json->{results} }) {
my $sitedomain = "$mdata->{custom_fields}->{Domain}";
my $routerip = "$mdata->{custom_fields}->{RouterIP}";
#vars
my $domain = $sitedomain;
my $host = $routerip;
print $domain;
print $host;
}
Print $host variable
print $host;
192.168.201.1192.168.202.1192.168.203.1
Print $domain variable
print $domain;
site1.global.localsite2.global.localsite3.global.local
JSON (test.json)
{
"results": [
{
"id": 37,
"url": "http://global.local/api/dcim/sites/37/",
"display": "Site 1",
"name": "Site 1",
"slug": "site1",
"custom_fields": {
"Domain": "site1.global.local",
"RouterIP": "192.168.201.1"
}
},
{
"id": 38,
"url": "http://global.local/api/dcim/sites/38/",
"display": "Site 2",
"name": "Site 2",
"slug": "site2",
"custom_fields": {
"Domain": "site2.global.local",
"RouterIP": "192.168.202.1"
}
},
{
"id": 39,
"url": "http://global.local/api/dcim/sites/39/",
"display": "Site 3",
"name": "Site 3",
"slug": "site3",
"custom_fields": {
"Domain": "site3.global.local",
"RouterIP": "192.168.203.1"
}
}
]
}
Your code produces expected result if you add \n to print statement. You can utilize say instead of print if there is no format required.
use warnings;
use strict;
use feature 'say';
use JSON::XS;
my $res = "test.json";
my $txt = do {
local $/;
open my $fh, "<", $res or die $!;
<$fh>;
};
my $json = decode_json($txt);
for my $mdata (#{ $json->{results} }) {
my $sitedomain = "$mdata->{custom_fields}->{Domain}";
my $routerip = "$mdata->{custom_fields}->{RouterIP}";
#vars
my $domain = $sitedomain;
my $host = $routerip;
say "$domain $host";
}
The code can be re-written in shorter form as following
use strict;
use warnings;
use feature 'say';
use JSON;
my $fname = 'router_test.json';
my $txt = do {
local $/;
open my $fh, "<", $fname or die $!;
<$fh>;
};
my $json = from_json($txt);
say "$_->{custom_fields}{Domain} $_->{custom_fields}{RouterIP}" for #{$json->{results}};
It sounds like you want to "slice" the data. You could buffer in code, or collect unique values later. Let's modify what you started with, and make some tweaks:
n.b. No need to quote my $sitedomain = "$mdata->{custom_fields}->{Domain}";. The content of the JSON is already a string, and forcing Perl to make another string by interpolating it is unnecessary.
n.b.2 JSON::XS works automatically if it's installed.
my %domains;
my %ips;
for my $mdata (#{ $json->{results} }) {
my $sitedomain = $mdata->{custom_fields}->{Domain};
my $routerip = $mdata->{custom_fields}->{RouterIP};
# Collect and count all the unique domains and IPs by storing them as hash keys
$domains{$sitedomain} += 1;
$ips{$routerip} += 1;
}
for my $key (keys %domains) {
printf "%s %s\n", $key, $domains{$key};
# and so on
}
If we don't know the custom fields, we can play with nested hashes to collect it all:
my %fields;
for my $mdata (#{ $json->{results} }) {
for my $custom_field (keys %{ $mdata->{custom_fields} }) {
$fields{$custom_field}{$mdata->{custom_fields}{$custom_field}} += 1;
}
}
for my $custom_field (keys %fields) {
print "$custom_field:\n";
for my $unique_value (keys %{ $fields{$custom_field} }){
printf "%s - %s\n", $unique_value, $fields{$custom_field}{$unique_value};
}
}
Example output:
RouterIP:
192.168.201.1 - 1
192.168.203.1 - 1
192.168.202.1 - 1
Domain:
site2.global.local - 1
site1.global.local - 1
site3.global.local - 1
... or something like that. Nested structures lead very quickly to messy code. You can mitigate it by dereferencing the substructures. It could also be more predictable if we work with a known list of keys e.g.
my #known_keys = qw/RouterIP Domain/;
for my $mdata (#{ $json->{results} }) {
for my $custom_field (#known_keys) {
if (exists $fields{$custom_field}) {
$fields{$custom_field}{$mdata->{custom_fields}{$custom_field}} += 1;
}
}
}
If the JSON file is massive you may run out of memory. For this you would need to look into a package like JSON::SL or JSON::Streaming::Reader. They're more involved to use but prevent you from needing to load the whole file into memory. There are also unix tools like jq that provide the same powers.

JSON how can I check if keys and array exists?

I have a JSON file, and I want to check if keys exists or not, if keys are empty or not.
I've already done this kind of check in the script below.
But, here I have "children" which is an empty array.
How can I see if this array exists or not and if this array is empty or not?
Here the JSON sample:
{
"id": "Store::STUDIO",
"name": "Studio",
"categories": [
{
"id": "Category::556",
"name": "Cinéma",
"children": []
},
{
"id": "Category::557",
"name": "Séries",
"children": []
}
],
"images": [
{
"format": "iso",
"url": "http://archive.ubuntu.com/ubuntu/dists/bionic-updates/main/installer-amd64/current/images/netboot/mini.iso",
"withTitle": false
}
],
"type": "PLAY"
}
Here is the script:
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use JSON qw( decode_json );
use JSON qw( from_json );
# JSON file
my $json_f = '/home/test';
# Variable
my $curl_cmd = "curl -o /dev/null --silent -Iw '%{http_code}'";
# JSON text
my $json_text = do {
open (TOP, "<", $json_f);
local $/;
<TOP>
};
my $decoded = from_json($json_text);
# Display value provider if exist
my $provider = $decoded->{"categories"}[0]{"provider"};
print $provider, "\n" if scalar $provider;
# Display value children if exist
my #child = $decoded->{"categories"}[0]{"children"};
print $child[0], "\n" if scalar #child;
# Checking an url is exist
my $url_src = $decoded->{"images"}[0]{"url"};
my $http_res = qx{$curl_cmd $url_src}; # Checking if URL is correct
# Display categories with others values
my #categories = #{ $decoded->{'categories'} };
foreach my $f ( #categories ) {
print $decoded->{"id"} . "|" . $f->{"id"} . "|" . $f->{"name"} . "|" . $http_res . "\n";
}
In your code, #child is an array of arrays. Dereference the array. Change:
my #child = $decoded->{"categories"}[0]{"children"};
to:
my #child = #{ $decoded->{"categories"}[0]{"children"} };

Perl LWP::UserAgent parse response JSON

I am using the LWP::UserAgent module to issue a GET request to one of our APIs.
#!/usr/bin/perl
use strict;
use warning;
use LWP::UserAgent;
use Data::Dumper;
my $ua = LWP::UserAgent->new;
my $request = $ua->get("http://example.com/foo", Authorization => "Bearer abc123", Accept => "application/json" );
print Dumper $request->content;
The request is successful. Dumper returns the following JSON.
$VAR1 = '{
"apiVersion": "v1",
"data": {
"ca-bundle.crt": "-----BEGIN CERTIFICATE-----abc123-----END CERTIFICATE-----\\n"
},
"kind": "ConfigMap",
"metadata": {
"creationTimestamp": "2021-07-16T17:13:01Z",
"labels": {
"auth.openshift.io/managed-certificate-type": "ca-bundle"
},
"managedFields": [
{
"apiVersion": "v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:ca-bundle.crt": {}
},
"f:metadata": {
"f:labels": {
".": {},
"f:auth.openshift.io/managed-certificate-type": {}
}
}
},
"manager": "cluster-kube-apiserver-operator",
"operation": "Update",
"time": "2021-09-14T17:07:39Z"
}
],
"name": "kube-control-plane-signer-ca",
"namespace": "openshift-kube-apiserver-operator",
"resourceVersion": "65461225",
"selfLink": "/api/v1/namespaces/openshift-kube-apiserver-operator/configmaps/kube-control-plane-signer-ca",
"uid": "f9aea067-1234-5678-9101-9d4073f5ae53"
}
}';
Let's say I want to print the value of the apiVersion key, which should print v1.
print "API Version = $request->content->{'apiVersion'} \n";
The following is being printed. I am not sure how to print the value v1. Since HTTP::Response is included in the output, I suspect I might have to use the HTTP::Response module?
API Version = HTTP::Response=HASH(0x2dffe80)->content->{'apiVersion'}
Perl doesn't expand subroutine calls in a double-quoted string.
print "API Version = $request->content->{'apiVersion'} \n";
In this line of code, content() is a subroutine call. So Perl sees this as:
print "API Version = $request" . "->content->{'apiVersion'} \n";
And if you try to print most Perl objects, you'll get the hash reference along with the name of the class - hence HTTP::Response=HASH(0x2dffe80).
You might think that you just need to break up your print() statement like this:
print 'API Version = ', $request->content->{'apiVersion'}, "\n";
But that's not going to work either. $request->content doesn't return a Perl data structure, it returns a JSON-encoded string. You need to decode it into a data structure before you can access the individual elements.
use JSON;
print 'API Version = ', decode_json($request->content)->{'apiVersion'}, "\n";
But it might be cleaner to do the decoding outside of the print() statement.
use JSON;
my $data = decode_json($request->content);
In which case you can go back to something more like your original code:
print "API Version = $data->{'apiVersion'} \n";
The JSON content must be decoded first. There are several modules for that, like JSON:
use JSON;
# ...
my $href = decode_json $request->content;
And then use it like a normal hash reference: $href->{apiVersion}

looping through json in perl

I'm trying to grab some information out of a json export from Ping. My rusty Perl skills are failing me as I'm getting lost in the weeds with the dereferencing. Rather than bang my head against the wall some more I thought I'd post a question since all the google searches are leading here.
My understanding is that decode_json converts items into an array of hashes and each hash has strings and some other arrays of hashes as contents. This seems to bear out when attempting to get to an individual string value but only if I manually specify a specific array element. I can't figure out how to loop through the items.
The JSON comes back like this:
{
"items":[
{
#lots of values here are some examples
"type": "SP",
"contactInfo": {
"company": "Acme",
"email": "john.doe#acme.com"
}
]
}
I had no problems getting to actual values
#!/usr/bin/perl
use JSON;
use Data::Dumper;
use strict;
use warnings;
use LWP::Simple;
my $json;
{
local $/; #Enable 'slurp' mode
open my $fh, "<", "idp.json";
$json = <$fh>;
close $fh;
}
my $data = decode_json($json);
#array print $data->{'items'};
#hash print $data->{'items'}->[0];
#print $data->{'items'}->[0]->{'type'};
But, I can't figure out how to iterate through the array of items. I've tried for and foreach and various combinations of dereferencing, and it keeps telling me that the value I'm looping thru is still an array. If $data->{'items'} is an array, then presumably I should be able to do some variation of
foreach my $item ($data->{'items'})
or
my #items = $data->{'items'};
for (#items)
{
# stuff
}
But, I keep getting arrays back and I have to add in the ->[0] to get to a specific value.
$data->{'items'} is a reference to an array (of hash references). You need to dereference it, with #{ }:
use JSON;
use strict;
use warnings;
my $json;
{
local $/; #Enable 'slurp' mode
$json = <DATA>;
}
my $data = decode_json($json);
for my $item (#{ $data->{items} }) {
print "$item->{type}\n";
}
__DATA__
{
"items":[
{
"type": "SP",
"contactInfo": {
"company": "Acme",
"email": "john.doe#acme.com"
}
}
]
}
Output:
SP

How can I stream JSON from a file?

I will have a possibly very large JSON file and I want to stream from it instead of load it all into memory. Based on the following statement (I added the emphasis) from JSON::XS, I believe it won't suit my needs. Is there a Perl 5 JSON module that will stream the results from the disk?
In some cases, there is the need for incremental parsing of JSON texts. While this module always has to keep both JSON text and resulting Perl data structure in memory at one time, it does allow you to parse a JSON stream incrementally. It does so by accumulating text until it has a full JSON object, which it then can decode. This process is similar to using decode_prefix to see if a full JSON object is available, but is much more efficient (and can be implemented with a minimum of method calls).
To clarify, the JSON will contain an array of objects. I want to read one object at a time from the file.
In terms of ease of use and speed, JSON::SL seems to be the winner:
#!/usr/bin/perl
use strict;
use warnings;
use JSON::SL;
my $p = JSON::SL->new;
#look for everthing past the first level (i.e. everything in the array)
$p->set_jsonpointer(["/^"]);
local $/ = \5; #read only 5 bytes at a time
while (my $buf = <DATA>) {
$p->feed($buf); #parse what you can
#fetch anything that completed the parse and matches the JSON Pointer
while (my $obj = $p->fetch) {
print "$obj->{Value}{n}: $obj->{Value}{s}\n";
}
}
__DATA__
[
{ "n": 0, "s": "zero" },
{ "n": 1, "s": "one" },
{ "n": 2, "s": "two" }
]
JSON::Streaming::Reader was okay, but it is slower and suffers from too verbose an interface (all of these coderefs are required even though many do nothing):
#!/usr/bin/perl
use strict;
use warnings;
use JSON::Streaming::Reader;
my $p = JSON::Streaming::Reader->for_stream(\*DATA);
my $obj;
my $attr;
$p->process_tokens(
start_array => sub {}, #who cares?
end_array => sub {}, #who cares?
end_property => sub {}, #who cares?
start_object => sub { $obj = {}; }, #clear the current object
start_property => sub { $attr = shift; }, #get the name of the attribute
#add the value of the attribute to the object
add_string => sub { $obj->{$attr} = shift; },
add_number => sub { $obj->{$attr} = shift; },
#object has finished parsing, it can be used now
end_object => sub { print "$obj->{n}: $obj->{s}\n"; },
);
__DATA__
[
{ "n": 0, "s": "zero" },
{ "n": 1, "s": "one" },
{ "n": 2, "s": "two" }
]
To parse 1,000 records it took JSON::SL .2 seconds and JSON::Streaming::Reader 3.6 seconds (note, JSON::SL was being fed 4k at a time, I had no control over JSON::Streaming::Reader's buffer size).
Have you looked at JSON::Streaming::Reader which shows up as first while searching for 'JSON Stream' on search.cpan.org?
Alternatively JSON::SL found by searching for 'JSON SAX' - not quite as obvious search terms, but what you describe sounds like a SAX parsers for XML.
It does so by accumulating text until it has a full JSON object, which it then can decode.
This is what screws your over. A JSON document is one object.
You need to define more clearly what you want from incremental parsing. Are you looking for one element of a large mapping? What are you trying to do with the information you read out/write?
I don't know any library that will incrementally parse JSON data by reading one element out of an array at once. However this is quite simple to implement yourself using a finite state automaton (basically your file has the format \s*\[\s*([^,]+,)*([^,]+)?\s*\]\s* except that you need to parse commas in strings correctly.)
Did you try to skip first right braket [ and then the commas , :
$json->incr_text =~ s/^ \s* \[ //x;
...
$json->incr_text =~ s/^ \s* , //x;
...
$json->incr_text =~ s/^ \s* \] //x;
like in the third example :
http://search.cpan.org/dist/JSON-XS/XS.pm#EXAMPLES
If you have control over how you're generating your JSON, then I suggest turning pretty formatting off and printing one object per line. This makes parsing simple, like so:
use Data::Dumper;
use JSON::Parse 'json_to_perl';
use JSON;
use JSON::SL;
my $json_sl = JSON::SL->new();
use JSON::XS;
my $json_xs = JSON::XS->new();
$json_xs = $json_xs->pretty(0);
#$json_xs = $json_xs->utf8(1);
#$json_xs = $json_xs->ascii(0);
#$json_xs = $json_xs->allow_unknown(1);
my ($file) = #ARGV;
unless( defined $file && -f $file )
{
print STDERR "usage: $0 FILE\n";
exit 1;
}
my #cmd = ( qw( CMD ARGS ), $file );
open my $JSON, '-|', #cmd or die "Failed to exec #cmd: $!";
# local $/ = \4096; #read 4k at a time
while( my $line = <$JSON> )
{
if( my $obj = json($line) )
{
print Dumper($obj);
}
else
{
die "error: failed to parse line - $line";
}
exit if( $. == 5 );
}
exit 0;
sub json
{
my ($data) = #_;
return decode_json($data);
}
sub json_parse
{
my ($data) = #_;
return json_to_perl($data);
}
sub json_xs
{
my ($data) = #_;
return $json_xs->decode($data);
}
sub json_xs_incremental
{
my ($data) = #_;
my $result = [];
$json_xs->incr_parse($data); # void context, so no parsing
push( #$result, $_ ) for( $json_xs->incr_parse );
return $result;
}
sub json_sl_incremental
{
my ($data) = #_;
my $result = [];
$json_sl->feed($data);
push( #$result, $_ ) for( $json_sl->fetch );
# ? error: JSON::SL - Got error CANT_INSERT at position 552 at json_to_perl.pl line 82, <$JSON> line 2.
return $result;
}