Skip to content

Instantly share code, notes, and snippets.

@YogaSakti
Created January 8, 2026 03:59
Show Gist options
  • Select an option

  • Save YogaSakti/9f1f42c075cefe4043aac67298b07e08 to your computer and use it in GitHub Desktop.

Select an option

Save YogaSakti/9f1f42c075cefe4043aac67298b07e08 to your computer and use it in GitHub Desktop.
ImportJSON by Yoga Sakti, Brad Jasper and Trevor Lohrbeer - Enhanced Version
/*====================================================================================================================================*
ImportJSON by Yoga Sakti, Brad Jasper and Trevor Lohrbeer - Enhanced Version
====================================================================================================================================
Version: 2.0.0
Project Page: https://github.com/bradjasper/ImportJSON
Copyright: (c) 2025-2026 by Yoga Sakti
(c) 2017-2019 by Brad Jasper
(c) 2012-2017 by Trevor Lohrbeer
License: GNU General Public License, version 3 (GPL-3.0)
http://www.opensource.org/licenses/gpl-3.0.html
------------------------------------------------------------------------------------------------------------------------------------
Enhanced version with modern Google Apps Script features and better error handling
Based on original version 1.5.0 from https://github.com/bradjasper/ImportJSON
Changelog:
2.0.0 (January 2026) - Major rewrite with bug fixes
- Added try-catch error handling, URL security validation, HTTP response validation
- Added new options: keepNull, customSeparator, maxLength
- Fixed isObjectArray_() empty array crash, findEqualityEndpoint_() infinite loop bug
- Fixed hasOption_() type-safety, deprecated substr() replaced with substring()
- Replaced optional chaining (?.) for older GAS compatibility
- Modernized code: const/let, arrow functions, Object.assign(), template literals
1.7.0 (2025) - Enhanced error handling, URL validation, null handling, and modern JS features
------------------------------------------------------------------------------------------------------------------------------------
Original Changelog (from bradjasper/ImportJSON):
1.6.0 (June 2, 2019) Fixed null values (thanks @gdesmedt1)
1.5.0 (January 11, 2019) Adds ability to include all headers in a fixed order
1.4.0 (July 23, 2017) Transfer project to Brad Jasper. Fixed bugs. Added ImportJSONFromSheet and ImportJSONBasicAuth.
1.3.0 Adds ability to import the text from a set of rows containing the text to parse
1.2.1 Fixed a bug with how nested arrays are handled
1.2.0 Added ImportJSONViaPost and support for fetchOptions
1.1.1 Added version number using Google Scripts Versioning
1.1.0 Added support for the noHeaders option
1.0.0 Initial release
*====================================================================================================================================*/
/**
* Imports a JSON feed and returns the results to be inserted into a Google Spreadsheet. The JSON feed is flattened to create
* a two-dimensional array. The first row contains the headers, with each column header indicating the path to that data in
* the JSON feed. The remaining rows contain the data.
*
* @param {string} url - the URL to a public JSON feed
* @param {string} query - a comma-separated list of paths to import
* @param {string} parseOptions - a comma-separated list of options that alter processing of the data
* @customfunction
* @return {Array<Array>} a two-dimensional array containing the data, with the first row containing headers
*/
function ImportJSON(url, query, parseOptions) {
try {
validateUrl_(url);
return ImportJSONAdvanced(url, null, query, parseOptions, includeXPath_, defaultTransform_);
} catch (error) {
console.error('ImportJSON Error:', error.toString());
return [['Error'], [error.toString()]];
}
}
/**
* Enhanced version with better error handling and security
*/
function ImportJSONAdvanced(url, fetchOptions, query, parseOptions, includeFunc, transformFunc) {
try {
// Input validation
if (!url || typeof url !== 'string') {
throw new Error('Invalid URL provided');
}
validateUrl_(url);
// Enhanced fetch options with better defaults
const defaultFetchOptions = {
muteHttpExceptions: true,
validateHttpsCertificates: true,
followRedirects: true,
timeout: 30000 // 30 second timeout
};
const finalFetchOptions = Object.assign(defaultFetchOptions, fetchOptions || {});
// Fetch with error handling
const response = UrlFetchApp.fetch(url, finalFetchOptions);
const responseCode = response.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
throw new Error(`HTTP Error ${responseCode}: ${response.getContentText()}`);
}
const jsonText = response.getContentText();
if (!jsonText || jsonText.trim() === '') {
throw new Error('Empty response received from URL');
}
let object;
try {
object = JSON.parse(jsonText);
} catch (parseError) {
throw new Error(`Invalid JSON format: ${parseError.message}`);
}
return parseJSONObject_(object, query, parseOptions, includeFunc, transformFunc);
} catch (error) {
console.error('ImportJSONAdvanced Error:', error.toString());
return [['Error'], [error.toString()]];
}
}
/**
* Enhanced ImportJSONViaPost with better error handling
*/
function ImportJSONViaPost(url, payload, fetchOptions, query, parseOptions) {
try {
validateUrl_(url);
const postOptions = parseToObject_(fetchOptions);
// Set defaults with null coalescing
postOptions.method = postOptions.method ?? "POST";
postOptions.payload = postOptions.payload ?? payload;
postOptions.contentType = postOptions.contentType ?? "application/x-www-form-urlencoded";
// Convert boolean options safely
['validateHttpsCertificates', 'useIntranet', 'followRedirects', 'muteHttpExceptions']
.forEach(key => convertToBool_(postOptions, key));
return ImportJSONAdvanced(url, postOptions, query, parseOptions, includeXPath_, defaultTransform_);
} catch (error) {
console.error('ImportJSONViaPost Error:', error.toString());
return [['Error'], [error.toString()]];
}
}
/**
* Enhanced ImportJSONFromSheet with better error handling
*/
function ImportJSONFromSheet(sheetName, query, options) {
try {
if (!sheetName || typeof sheetName !== 'string') {
throw new Error('Invalid sheet name provided');
}
const object = getDataFromNamedSheet_(sheetName);
return parseJSONObject_(object, query, options, includeXPath_, defaultTransform_);
} catch (error) {
console.error('ImportJSONFromSheet Error:', error.toString());
return [['Error'], [error.toString()]];
}
}
/**
* Enhanced Basic Auth function with better security
*/
function ImportJSONBasicAuth(url, username, password, query, parseOptions) {
try {
validateUrl_(url);
if (!username || !password) {
throw new Error('Username and password are required for basic auth');
}
const encodedAuthInformation = Utilities.base64Encode(`${username}:${password}`);
const header = {
headers: {
Authorization: `Basic ${encodedAuthInformation}`,
'User-Agent': 'Google Apps Script ImportJSON'
}
};
return ImportJSONAdvanced(url, header, query, parseOptions, includeXPath_, defaultTransform_);
} catch (error) {
console.error('ImportJSONBasicAuth Error:', error.toString());
return [['Error'], [error.toString()]];
}
}
/**
* Enhanced URL validation function
*/
function validateUrl_(url) {
if (!url || typeof url !== 'string') {
throw new Error('URL must be a non-empty string');
}
// Basic URL validation
const urlPattern = /^https?:\/\/.+/i;
if (!urlPattern.test(url)) {
throw new Error('URL must start with http:// or https://');
}
// Check for suspicious patterns (basic security)
const suspiciousPatterns = [
/javascript:/i,
/data:/i,
/file:/i,
/ftp:/i
];
if (suspiciousPatterns.some(pattern => pattern.test(url))) {
throw new Error('URL contains suspicious protocol');
}
}
/**
* Enhanced JSON parsing with better null handling
*/
function parseJSONObject_(object, query, options, includeFunc, transformFunc) {
try {
const headers = {};
const data = [];
// Handle query parameter conversion
if (query && !Array.isArray(query) && query.toString().indexOf(",") !== -1) {
query = query.toString().split(",").map(q => q.trim()).filter(q => q.length > 0);
}
// Prepopulate headers for consistent ordering
if (hasOption_(options, "allHeaders") && Array.isArray(query)) {
query.forEach((q, i) => {
headers[q] = i;
});
}
const optionsArray = options ? options.toString().split(",").map(o => o.trim()) : [];
parseData_(headers, data, "", {rowIndex: 1}, object, query, optionsArray, includeFunc);
parseHeaders_(headers, data);
transformData_(data, optionsArray, transformFunc);
return hasOption_(optionsArray, "noHeaders") ? (data.length > 1 ? data.slice(1) : []) : data;
} catch (error) {
console.error('parseJSONObject_ Error:', error.toString());
return [['Parse Error'], [error.toString()]];
}
}
/**
* Enhanced data parsing with better null/undefined handling
*/
function parseData_(headers, data, path, state, value, query, options, includeFunc) {
let dataInserted = false;
// Better null/undefined handling
if (value === null || value === undefined) {
if (!includeFunc || includeFunc(query, path, options)) {
if (!data[state.rowIndex]) {
data[state.rowIndex] = [];
}
if (!(path in headers)) {
headers[path] = Object.keys(headers).length;
}
// Handle null values based on search results suggestions
data[state.rowIndex][headers[path]] = hasOption_(options, "keepNull") ? null : "";
dataInserted = true;
}
} else if (Array.isArray(value) && isObjectArray_(value)) {
for (let i = 0; i < value.length; i++) {
if (parseData_(headers, data, path, state, value[i], query, options, includeFunc)) {
dataInserted = true;
if (data[state.rowIndex]) {
state.rowIndex++;
}
}
}
} else if (isObject_(value)) {
for (const key in value) {
if (value.hasOwnProperty(key)) {
if (parseData_(headers, data, `${path}/${key}`, state, value[key], query, options, includeFunc)) {
dataInserted = true;
}
}
}
} else if (!includeFunc || includeFunc(query, path, options)) {
// Handle arrays containing only scalar values
if (Array.isArray(value)) {
value = value.join(hasOption_(options, "customSeparator") ?
getOptionValue_(options, "customSeparator") : ",");
}
// Insert new row if one doesn't already exist
if (!data[state.rowIndex]) {
data[state.rowIndex] = [];
}
// Add a new header if one doesn't exist
if (!(path in headers)) {
headers[path] = Object.keys(headers).length;
}
// Insert the data with type preservation
data[state.rowIndex][headers[path]] = value;
dataInserted = true;
}
return dataInserted;
}
/**
* Enhanced default transformation with better null handling
*/
function defaultTransform_(data, row, column, options) {
// Enhanced null handling based on search results
if (data[row][column] === null || data[row][column] === undefined) {
if (row < 2 || hasOption_(options, "noInherit")) {
data[row][column] = hasOption_(options, "keepNull") ? null : "";
} else {
data[row][column] = data[row-1][column] ?? "";
}
}
if (!hasOption_(options, "rawHeaders") && row === 0) {
if (column === 0 && data[row].length > 1) {
removeCommonPrefixes_(data, row);
}
data[row][column] = toTitleCase_(String(data[row][column]).replace(/[\/\_]/g, " "));
}
if (!hasOption_(options, "noTruncate") && data[row][column]) {
const maxLength = parseInt(getOptionValue_(options, "maxLength"), 10) || 256;
data[row][column] = String(data[row][column]).substring(0, maxLength);
}
if (hasOption_(options, "debugLocation")) {
data[row][column] = `[${row},${column}]${data[row][column]}`;
}
}
/**
* Helper function to get option values
*/
function getOptionValue_(options, optionName) {
if (!options || !Array.isArray(options)) return null;
for (const option of options) {
if (option.includes('=') && option.startsWith(optionName + '=')) {
return option.split('=')[1];
}
}
return null;
}
/**
* Enhanced sheet data extraction with better error handling
*/
function getDataFromNamedSheet_(sheetName) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const source = ss.getSheetByName(sheetName);
if (!source) {
throw new Error(`Sheet '${sheetName}' not found`);
}
const lastRow = source.getLastRow();
if (lastRow === 0) {
throw new Error(`Sheet '${sheetName}' is empty`);
}
const jsonRange = source.getRange(1, 1, lastRow);
const jsonValues = jsonRange.getValues();
let jsonText = "";
for (const row of jsonValues) {
for (const cellValue of row) {
if (cellValue !== null && cellValue !== undefined) {
jsonText += cellValue;
}
}
}
if (!jsonText.trim()) {
throw new Error(`No JSON data found in sheet '${sheetName}'`);
}
console.log('JSON Text from sheet:', jsonText);
return JSON.parse(jsonText);
} catch (error) {
throw new Error(`Error reading from sheet '${sheetName}': ${error.message}`);
}
}
// Keep all other existing helper functions but with minor improvements for modern JavaScript
function URLEncode(value) {
return encodeURIComponent(String(value));
}
function isObject_(test) {
return test !== null && typeof test === 'object' && !Array.isArray(test);
}
function isObjectArray_(test) {
if (!test || test.length === 0) return false;
return test.some(item => isObject_(item));
}
function toTitleCase_(text) {
if (text === null || text === undefined) {
return "";
}
return String(text).replace(/\w\S*/g, (word) =>
word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()
);
}
function hasOption_(options, option) {
if (!options) return false;
if (Array.isArray(options)) {
return options.indexOf(option) >= 0;
}
return String(options).split(',').map(o => o.trim()).indexOf(option) >= 0;
}
function parseToObject_(text) {
const map = {};
const entries = (text && text.trim().length > 0) ? text.toString().split(",") : [];
entries.forEach(entry => addToMap_(map, entry));
return map;
}
function addToMap_(map, entry) {
const equalsIndex = entry.indexOf("=");
const key = (equalsIndex !== -1) ? entry.substring(0, equalsIndex) : entry;
const value = (key.length + 1 < entry.length) ? entry.substring(key.length + 1) : "";
map[key.trim()] = value;
}
function toBool_(value) {
return value !== null && value !== undefined && value.toString().toLowerCase() === "true";
}
function convertToBool_(map, key) {
if (map[key] !== null && map[key] !== undefined) {
map[key] = toBool_(map[key]);
}
}
// Keep all other existing functions with the same logic but enhanced error handling
function includeXPath_(query, path, options) {
if (!query) {
return true;
} else if (Array.isArray(query)) {
return query.some(q => applyXPathRule_(q, path, options));
} else {
return applyXPathRule_(query, path, options);
}
}
function applyXPathRule_(rule, path, options) {
if (path.indexOf(rule) === 0) {
return true;
}
if (rule.indexOf("/*") === -1) {
return false;
}
const rulePieces = rule.split("/");
const pathPieces = path.split("/");
if (rulePieces.length !== pathPieces.length) {
return false;
}
return rulePieces.every((piece, i) => piece === "*" || piece === pathPieces[i]);
}
function parseHeaders_(headers, data) {
data[0] = [];
Object.keys(headers).forEach(key => {
data[0][headers[key]] = key;
});
}
function transformData_(data, options, transformFunc) {
if (!transformFunc) return;
const columnCount = (data[0] && data[0].length) ? data[0].length : 0;
for (let i = 0; i < data.length; i++) {
if (!data[i]) continue;
for (let j = 0; j < columnCount; j++) {
try {
transformFunc(data, i, j, options);
} catch (error) {
console.warn('Transform error at [' + i + ',' + j + ']:' + error.toString());
}
}
}
}
function removeCommonPrefixes_(data, row) {
if (!data[row] || data[row].length < 2) return;
let matchIndex = (data[row][0] && data[row][0].length) ? data[row][0].length : 0;
for (let i = 1; i < data[row].length; i++) {
matchIndex = findEqualityEndpoint_(data[row][i-1], data[row][i], matchIndex);
if (matchIndex === 0) return;
}
for (let i = 0; i < data[row].length; i++) {
if (data[row][i] && typeof data[row][i] === 'string') {
data[row][i] = data[row][i].substring(matchIndex);
}
}
}
function findEqualityEndpoint_(string1, string2, stopAt) {
if (!string1 || !string2) {
return 0;
}
const maxEndpoint = Math.min(stopAt, string1.length, string2.length);
for (let i = 0; i < maxEndpoint; i++) {
if (string1.charAt(i) !== string2.charAt(i)) {
return i;
}
}
return maxEndpoint;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment