/* JSON-to-Go by Matt Holt https://github.com/mholt/json-to-go A simple utility to translate JSON into a Go type definition. */ function jsonToGo(json, typename, flatten = true, example = false, allOmitempty = false) { let data; let scope; let go = ""; let tabs = 0; const seen = {}; const stack = []; let accumulator = ""; let innerTabs = 0; let parent = ""; let globallySeenTypeNames = []; let previousParents = ""; try { data = JSON.parse(json.replace(/(:\s*\[?\s*-?\d*)\.0/g, "$1.1")); // hack that forces floats to stay as floats scope = data; } catch (e) { return { go: "", error: e.message }; } typename = format(typename || "AutoGenerated"); append(`type ${typename} `); parseScope(scope); if (flatten) go += accumulator // add final newline for POSIX 3.206 if (!go.endsWith(`\n`)) go += `\n` return { go: go }; function parseScope(scope, depth = 0) { if (typeof scope === "object" && scope !== null) { if (Array.isArray(scope)) { let sliceType; const scopeLength = scope.length; for (let i = 0; i < scopeLength; i++) { const thisType = goType(scope[i]); if (!sliceType) sliceType = thisType; else if (sliceType != thisType) { sliceType = mostSpecificPossibleGoType(thisType, sliceType); if (sliceType == "any") break; } } const slice = flatten && ["struct", "slice"].includes(sliceType) ? `[]${parent}` : `[]`; if (flatten && depth >= 2) appender(slice); else append(slice) if (sliceType == "struct") { const allFields = {}; // for each field counts how many times appears for (let i = 0; i < scopeLength; i++) { const keys = Object.keys(scope[i]) for (let k in keys) { let keyname = keys[k]; if (!(keyname in allFields)) { allFields[keyname] = { value: scope[i][keyname], count: 0 } } else { const existingValue = allFields[keyname].value; const currentValue = scope[i][keyname]; if (!areSameType(existingValue, currentValue)) { if(existingValue !== null) { allFields[keyname].value = null // force type "any" if types are not identical console.warn(`Warning: key "${keyname}" uses multiple types. Defaulting to type "any".`) } allFields[keyname].count++ continue } // if variable was first detected as int (7) and a second time as float64 (3.14) // then we want to select float64, not int. Similar for int64 and float64. if(areSameType(currentValue, 1)) allFields[keyname].value = findBestValueForNumberType(existingValue, currentValue); if (areObjects(existingValue, currentValue)) { const comparisonResult = compareObjectKeys( Object.keys(currentValue), Object.keys(existingValue) ) if (!comparisonResult) { keyname = `${keyname}_${uuidv4()}`; allFields[keyname] = { value: currentValue, count: 0 }; } } } allFields[keyname].count++; } } // create a common struct with all fields found in the current array // omitempty dict indicates if a field is optional const keys = Object.keys(allFields), struct = {}, omitempty = {}; for (let k in keys) { const keyname = keys[k], elem = allFields[keyname]; struct[keyname] = elem.value; omitempty[keyname] = elem.count != scopeLength; } parseStruct(depth + 1, innerTabs, struct, omitempty, previousParents); // finally parse the struct !! } else if (sliceType == "slice") { parseScope(scope[0], depth) } else { if (flatten && depth >= 2) { appender(sliceType || "any"); } else { append(sliceType || "any"); } } } else { if (flatten) { if (depth >= 2){ appender(parent) } else { append(parent) } } parseStruct(depth + 1, innerTabs, scope, false, previousParents); } } else { if (flatten && depth >= 2){ appender(goType(scope)); } else { append(goType(scope)); } } } function parseStruct(depth, innerTabs, scope, omitempty, oldParents) { if (flatten) { stack.push( depth >= 2 ? "\n" : "" ) } const seenTypeNames = []; if (flatten && depth >= 2) { const parentType = `type ${parent}`; const scopeKeys = formatScopeKeys(Object.keys(scope)); // this can only handle two duplicate items // future improvement will handle the case where there could // three or more duplicate keys with different values if (parent in seen && compareObjectKeys(scopeKeys, seen[parent])) { stack.pop(); return } seen[parent] = scopeKeys; appender(`${parentType} struct {\n`); ++innerTabs; const keys = Object.keys(scope); previousParents = parent for (let i in keys) { const keyname = getOriginalName(keys[i]); indenter(innerTabs) let typename // structs will be defined on the top level of the go file, so they need to be globally unique if (typeof scope[keys[i]] === "object" && scope[keys[i]] !== null) { typename = uniqueTypeName(format(keyname), globallySeenTypeNames, previousParents) globallySeenTypeNames.push(typename) } else { typename = uniqueTypeName(format(keyname), seenTypeNames) seenTypeNames.push(typename) } appender(typename+" "); parent = typename parseScope(scope[keys[i]], depth); appender(' `json:"'+keyname); if (allOmitempty || (omitempty && omitempty[keys[i]] === true)) { appender(',omitempty'); } appender('"`\n'); } indenter(--innerTabs); appender("}"); previousParents = oldParents; } else { append("struct {\n"); ++tabs; const keys = Object.keys(scope); previousParents = parent for (let i in keys) { const keyname = getOriginalName(keys[i]); indent(tabs); let typename // structs will be defined on the top level of the go file, so they need to be globally unique if (typeof scope[keys[i]] === "object" && scope[keys[i]] !== null) { typename = uniqueTypeName(format(keyname), globallySeenTypeNames, previousParents) globallySeenTypeNames.push(typename) } else { typename = uniqueTypeName(format(keyname), seenTypeNames) seenTypeNames.push(typename) } append(typename+" "); parent = typename parseScope(scope[keys[i]], depth); append(' `json:"'+keyname); if (allOmitempty || (omitempty && omitempty[keys[i]] === true)) { append(',omitempty'); } if (example && scope[keys[i]] !== "" && typeof scope[keys[i]] !== "object") { append('" example:"'+scope[keys[i]]) } append('"`\n'); } indent(--tabs); append("}"); previousParents = oldParents; } if (flatten) accumulator += stack.pop(); } function indent(tabs) { for (let i = 0; i < tabs; i++) go += '\t'; } function append(str) { go += str; } function indenter(tabs) { for (let i = 0; i < tabs; i++) stack[stack.length - 1] += '\t'; } function appender(str) { stack[stack.length - 1] += str; } // Generate a unique name to avoid duplicate struct field names. // This function appends a number at the end of the field name. function uniqueTypeName(name, seen, prefix=null) { if (seen.indexOf(name) === -1) { return name; } // check if we can get a unique name by prefixing it if(prefix) { name = prefix+name if (seen.indexOf(name) === -1) { return name; } } let i = 0; while (true) { let newName = name + i.toString(); if (seen.indexOf(newName) === -1) { return newName; } i++; } } // Sanitizes and formats a string to make an appropriate identifier in Go function format(str) { str = formatNumber(str); let sanitized = toProperCase(str).replace(/[^a-z0-9]/ig, "") if (!sanitized) { return "NAMING_FAILED"; } // After sanitizing the remaining characters can start with a number. // Run the sanitized string again trough formatNumber to make sure the identifier is Num[0-9] or Zero_... instead of 1. return formatNumber(sanitized) } // Adds a prefix to a number to make an appropriate identifier in Go function formatNumber(str) { if (!str) return ""; else if (str.match(/^\d+$/)) str = "Num" + str; else if (str.charAt(0).match(/\d/)) { const numbers = {'0': "Zero_", '1': "One_", '2': "Two_", '3': "Three_", '4': "Four_", '5': "Five_", '6': "Six_", '7': "Seven_", '8': "Eight_", '9': "Nine_"}; str = numbers[str.charAt(0)] + str.substr(1); } return str; } // Determines the most appropriate Go type function goType(val) { if (val === null) return "any"; switch (typeof val) { case "string": if (/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(\+\d\d:\d\d|Z)$/.test(val)) return "time.Time"; else return "string"; case "number": if (val % 1 === 0) { if (val > -2147483648 && val < 2147483647) return "int"; else return "int64"; } else return "float64"; case "boolean": return "bool"; case "object": if (Array.isArray(val)) return "slice"; return "struct"; default: return "any"; } } // change the value to expand ints and floats to their larger equivalent function findBestValueForNumberType(existingValue, newValue) { if (!areSameType(newValue, 1)) { console.error(`Error: currentValue ${newValue} is not a number`) return null // falls back to goType "any" } const newGoType = goType(newValue) const existingGoType = goType(existingValue) if (newGoType === existingGoType) return existingValue // always upgrade float64 if (newGoType === "float64") return newValue if (existingGoType === "float64") return existingValue // it's too complex to distinguish int types and float32, so we force-upgrade to float64 // if anyone has a better suggestion, PRs are welcome! if (newGoType.includes("float") && existingGoType.includes("int")) return Number.MAX_VALUE if (newGoType.includes("int") && existingGoType.includes("float")) return Number.MAX_VALUE if (newGoType.includes("int") && existingGoType.includes("int")) { const existingValueAbs = Math.abs(existingValue); const newValueAbs = Math.abs(newValue); // if the sum is overflowing, it's safe to assume numbers are very large. So we force int64. if (!isFinite(existingValueAbs + newValueAbs)) return Number.MAX_SAFE_INTEGER // it's too complex to distinguish int8, int16, int32 and int64, so we just use the sum as best-guess return existingValueAbs + newValueAbs; } // There should be other cases console.error(`Error: something went wrong with findBestValueForNumberType() using the values: '${newValue}' and '${existingValue}'`) console.error(" Please report the problem to https://github.com/mholt/json-to-go/issues") return null // falls back to goType "any" } // Given two types, returns the more specific of the two function mostSpecificPossibleGoType(typ1, typ2) { if (typ1.substr(0, 5) == "float" && typ2.substr(0, 3) == "int") return typ1; else if (typ1.substr(0, 3) == "int" && typ2.substr(0, 5) == "float") return typ2; else return "any"; } // Proper cases a string according to Go conventions function toProperCase(str) { // ensure that the SCREAMING_SNAKE_CASE is converted to snake_case if (str.match(/^[_A-Z0-9]+$/)) { str = str.toLowerCase(); } // https://github.com/golang/lint/blob/5614ed5bae6fb75893070bdc0996a68765fdd275/lint.go#L771-L810 const commonInitialisms = [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" ]; return str.replace(/(^|[^a-zA-Z])([a-z]+)/g, function(unused, sep, frag) { if (commonInitialisms.indexOf(frag.toUpperCase()) >= 0) return sep + frag.toUpperCase(); else return sep + frag[0].toUpperCase() + frag.substr(1).toLowerCase(); }).replace(/([A-Z])([a-z]+)/g, function(unused, sep, frag) { if (commonInitialisms.indexOf(sep + frag.toUpperCase()) >= 0) return (sep + frag).toUpperCase(); else return sep + frag; }); } function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function getOriginalName(unique) { const reLiteralUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i const uuidLength = 36; if (unique.length >= uuidLength) { const tail = unique.substr(-uuidLength); if (reLiteralUUID.test(tail)) { return unique.slice(0, -1 * (uuidLength + 1)) } } return unique } function areObjects(objectA, objectB) { const object = "[object Object]"; return Object.prototype.toString.call(objectA) === object && Object.prototype.toString.call(objectB) === object; } function areSameType(objectA, objectB) { // prototype.toString required to compare Arrays and Objects const typeA = Object.prototype.toString.call(objectA) const typeB = Object.prototype.toString.call(objectB) return typeA === typeB } function compareObjectKeys(itemAKeys, itemBKeys) { const lengthA = itemAKeys.length; const lengthB = itemBKeys.length; // nothing to compare, probably identical if (lengthA == 0 && lengthB == 0) return true; // duh if (lengthA != lengthB) return false; for (let item of itemAKeys) { if (!itemBKeys.includes(item)) return false; } return true; } function formatScopeKeys(keys) { for (let i in keys) { keys[i] = format(keys[i]); } return keys } } if (typeof module != 'undefined') { if (!module.parent) { let filename = null function jsonToGoWithErrorHandling(json) { const output = jsonToGo(json) if (output.error) { console.error(output.error) process.exitCode = 1 } process.stdout.write(output.go) } process.argv.forEach((val, index) => { if (index < 2) return if (!val.startsWith('-')) { filename = val return } const argument = val.replace(/-/g, '') if (argument === "big") console.warn(`Warning: The argument '${argument}' has been deprecated and has no effect anymore`) else { console.error(`Unexpected argument ${val} received`) process.exit(1) } }) if (filename) { const fs = require('fs'); const json = fs.readFileSync(filename, 'utf8'); jsonToGoWithErrorHandling(json) return } if (!filename) { bufs = [] process.stdin.on('data', function(buf) { bufs.push(buf) }) process.stdin.on('end', function() { const json = Buffer.concat(bufs).toString('utf8') jsonToGoWithErrorHandling(json) }) return } } else { module.exports = jsonToGo } }