├── tests ├── array-with-nonmatching-types.go ├── double-nested-objects.json ├── array-with-nonmatching-types.json ├── array-with-mixed-float-int.go ├── double-nested-objects.go ├── duplicate-top-level-structs.json ├── array-with-mixed-float-int.json └── duplicate-top-level-structs.go ├── .github ├── FUNDING.yml └── workflows │ └── node-tests.yml ├── LICENSE ├── README.md ├── sample.json ├── json-to-go.test.js └── json-to-go.js /tests/array-with-nonmatching-types.go: -------------------------------------------------------------------------------- 1 | type AutoGenerated struct { 2 | Booleanfield bool `json:"booleanfield"` 3 | Somearray []Somearray `json:"somearray"` 4 | Date string `json:"date"` 5 | } 6 | type Somearray struct { 7 | ID any `json:"id"` 8 | Name string `json:"name"` 9 | Features any `json:"features"` 10 | } 11 | -------------------------------------------------------------------------------- /tests/double-nested-objects.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": { 3 | "id": 1, 4 | "type": { 5 | "short": "owa", 6 | "long": "objectwitharray" 7 | } 8 | }, 9 | "second": { 10 | "id": 2, 11 | "type": { 12 | "long": "object" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/array-with-nonmatching-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "booleanfield": true, 3 | "somearray": [ 4 | { 5 | "id": 1, 6 | "name": "John Doe", 7 | "features": { 8 | "age": 49, 9 | "height": 175 10 | } 11 | }, 12 | { 13 | "id": "2", 14 | "name": "John Doe", 15 | "features": [ 16 | "2", 17 | "3", 18 | "4" 19 | ] 20 | } 21 | ], 22 | "date": "2024-07-24" 23 | } 24 | -------------------------------------------------------------------------------- /tests/array-with-mixed-float-int.go: -------------------------------------------------------------------------------- 1 | type AutoGenerated struct { 2 | AgeOfTheUniverse []AgeOfTheUniverse `json:"age of the universe"` 3 | AgeOfTheEarth []AgeOfTheEarth `json:"age of the earth"` 4 | Date string `json:"date"` 5 | } 6 | type AgeOfTheUniverse struct { 7 | Name string `json:"name"` 8 | Value float64 `json:"value"` 9 | } 10 | type AgeOfTheEarth struct { 11 | Name string `json:"name"` 12 | Value int64 `json:"value"` 13 | } 14 | -------------------------------------------------------------------------------- /tests/double-nested-objects.go: -------------------------------------------------------------------------------- 1 | type AutoGenerated struct { 2 | First First `json:"first"` 3 | Second Second `json:"second"` 4 | } 5 | type Type struct { 6 | Short string `json:"short"` 7 | Long string `json:"long"` 8 | } 9 | type First struct { 10 | ID int `json:"id"` 11 | Type Type `json:"type"` 12 | } 13 | type SecondType struct { 14 | Long string `json:"long"` 15 | } 16 | type Second struct { 17 | ID int `json:"id"` 18 | SecondType SecondType `json:"type"` 19 | } 20 | -------------------------------------------------------------------------------- /tests/duplicate-top-level-structs.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": { 3 | "identifier": { 4 | "type": "ISO 3166-1", 5 | "id": 234 6 | }, 7 | "autonomous": true 8 | }, 9 | "municipality": { 10 | "identifier": { 11 | "type": "local", 12 | "name": "Tórshavn" 13 | } 14 | }, 15 | "building": { 16 | "identifier": { 17 | "postal": { 18 | "type": "local", 19 | "id": 100 20 | }, 21 | "road": { 22 | "name": "Gríms Kambansgøta", 23 | "id": 1 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /tests/array-with-mixed-float-int.json: -------------------------------------------------------------------------------- 1 | { 2 | "age of the universe": [ 3 | { 4 | "name": "in years", 5 | "value": 1378700000 6 | }, 7 | { 8 | "name": "in seconds", 9 | "value": 4.35075327952992e+17 10 | }, 11 | { 12 | "name": "in kapla", 13 | "value": 0.31914351851 14 | } 15 | ], 16 | "age of the earth": [ 17 | { 18 | "name": "in eons (roughly)", 19 | "value": 4 20 | }, 21 | { 22 | "name": "in years", 23 | "value": 4.543e9 24 | }, 25 | { 26 | "name": "in seconds", 27 | "value": 1.6592953e+12 28 | } 29 | ], 30 | "date": "2024-07-25" 31 | } 32 | -------------------------------------------------------------------------------- /tests/duplicate-top-level-structs.go: -------------------------------------------------------------------------------- 1 | type AutoGenerated struct { 2 | Region Region `json:"region"` 3 | Municipality Municipality `json:"municipality"` 4 | Building Building `json:"building"` 5 | } 6 | type Identifier struct { 7 | Type string `json:"type"` 8 | ID int `json:"id"` 9 | } 10 | type Region struct { 11 | Identifier Identifier `json:"identifier"` 12 | Autonomous bool `json:"autonomous"` 13 | } 14 | type MunicipalityIdentifier struct { 15 | Type string `json:"type"` 16 | Name string `json:"name"` 17 | } 18 | type Municipality struct { 19 | MunicipalityIdentifier MunicipalityIdentifier `json:"identifier"` 20 | } 21 | type Postal struct { 22 | Type string `json:"type"` 23 | ID int `json:"id"` 24 | } 25 | type Road struct { 26 | Name string `json:"name"` 27 | ID int `json:"id"` 28 | } 29 | type BuildingIdentifier struct { 30 | Postal Postal `json:"postal"` 31 | Road Road `json:"road"` 32 | } 33 | type Building struct { 34 | BuildingIdentifier BuildingIdentifier `json:"identifier"` 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Matt Holt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [JSON-to-Go converts JSON to a Go struct](https://mholt.github.io/json-to-go) 2 | 3 | Translates JSON into a Go type definition. [Check it out!](http://mholt.github.io/json-to-go) 4 | 5 | This is a sister tool to [curl-to-Go](https://mholt.github.io/curl-to-go), which converts curl commands to Go code. 6 | 7 | Things to note: 8 | 9 | - The script sometimes has to make some assumptions, so give the output a once-over. 10 | - In an array of objects, it is assumed that the first object is representative of the rest of them. 11 | - The output is indented, but not formatted. Use `go fmt`! 12 | 13 | Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. 14 | 15 | ### Usage 16 | 17 | - Read JSON file: 18 | 19 | ```sh 20 | node json-to-go.js sample.json 21 | ``` 22 | 23 | - Read JSON file from stdin: 24 | 25 | ```sh 26 | node json-to-go.js < sample.json 27 | cat sample.json | node json-to-go.js 28 | ``` 29 | 30 | ### Credits 31 | 32 | JSON-to-Go is brought to you by Matt Holt ([mholt6](https://twitter.com/mholt6)). 33 | 34 | The Go Gopher is originally by Renee French. This artwork is an adaptation. 35 | -------------------------------------------------------------------------------- /.github/workflows/node-tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: json-to-go tests 3 | 4 | on: # yamllint disable-line rule:truthy 5 | workflow_call: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - "master" 10 | - "main" 11 | pull_request: 12 | 13 | jobs: 14 | run-tests: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 22.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Run tests 28 | run: | 29 | node json-to-go.test.js 30 | 31 | - name: Run json-to-go using stdin 32 | shell: bash 33 | run: | 34 | set -eEuo pipefail 35 | got=$(node json-to-go.js < tests/double-nested-objects.json) 36 | exp=$(cat tests/double-nested-objects.go) 37 | echo "got: '${got}'" 38 | [[ "${got}" == "${exp}" ]] 39 | 40 | - name: Run json-to-go with a file 41 | shell: bash 42 | run: | 43 | set -eEuo pipefail 44 | got=$(node json-to-go.js tests/double-nested-objects.json) 45 | exp=$(cat tests/double-nested-objects.go) 46 | echo "got: '${got}'" 47 | [[ "${got}" == "${exp}" ]] 48 | 49 | - name: Check correct error handling using stdin 50 | shell: bash 51 | run: | 52 | ! node json-to-go.js <<< "error" 53 | 54 | - name: Check correct error handling with a file 55 | shell: bash 56 | run: | 57 | ! node json-to-go.js <(echo "error") 58 | -------------------------------------------------------------------------------- /sample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input_index": 0, 4 | "candidate_index": 0, 5 | "delivery_line_1": "1 N Rosedale St", 6 | "last_line": "Baltimore MD 21229-3737", 7 | "delivery_point_barcode": "212293737013", 8 | "components": { 9 | "primary_number": "1", 10 | "street_predirection": "N", 11 | "street_name": "Rosedale", 12 | "street_suffix": "St", 13 | "city_name": "Baltimore", 14 | "state_abbreviation": "MD", 15 | "zipcode": "21229", 16 | "plus4_code": "3737", 17 | "delivery_point": "01", 18 | "delivery_point_check_digit": "3" 19 | }, 20 | "metadata": { 21 | "record_type": "S", 22 | "zip_type": "Standard", 23 | "county_fips": "24510", 24 | "county_name": "Baltimore City", 25 | "carrier_route": "C047", 26 | "congressional_district": "07", 27 | "rdi": "Residential", 28 | "elot_sequence": "0059", 29 | "elot_sort": "A", 30 | "latitude": 39.28602, 31 | "longitude": -76.6689, 32 | "precision": "Zip9", 33 | "time_zone": "Eastern", 34 | "utc_offset": -5, 35 | "dst": true 36 | }, 37 | "analysis": { 38 | "dpv_match_code": "Y", 39 | "dpv_footnotes": "AABB", 40 | "dpv_cmra": "N", 41 | "dpv_vacant": "N", 42 | "active": "Y" 43 | } 44 | }, 45 | { 46 | "input_index": 0, 47 | "candidate_index": 1, 48 | "delivery_line_1": "1 S Rosedale St", 49 | "last_line": "Baltimore MD 21229-3739", 50 | "delivery_point_barcode": "212293739011", 51 | "components": { 52 | "primary_number": "1", 53 | "street_predirection": "S", 54 | "street_name": "Rosedale", 55 | "street_suffix": "St", 56 | "city_name": "Baltimore", 57 | "state_abbreviation": "MD", 58 | "zipcode": "21229", 59 | "plus4_code": "3739", 60 | "delivery_point": "01", 61 | "delivery_point_check_digit": "1" 62 | }, 63 | "metadata": { 64 | "record_type": "S", 65 | "zip_type": "Standard", 66 | "county_fips": "24510", 67 | "county_name": "Baltimore City", 68 | "carrier_route": "C047", 69 | "congressional_district": "07", 70 | "rdi": "Residential", 71 | "elot_sequence": "0064", 72 | "elot_sort": "A", 73 | "latitude": 39.2858, 74 | "longitude": -76.66889, 75 | "precision": "Zip9", 76 | "time_zone": "Eastern", 77 | "utc_offset": -5, 78 | "dst": true 79 | }, 80 | "analysis": { 81 | "dpv_match_code": "Y", 82 | "dpv_footnotes": "AABB", 83 | "dpv_cmra": "N", 84 | "dpv_vacant": "N", 85 | "active": "Y" 86 | } 87 | } 88 | ] -------------------------------------------------------------------------------- /json-to-go.test.js: -------------------------------------------------------------------------------- 1 | const jsonToGo = require("./json-to-go"); 2 | 3 | function quote(str) { 4 | return "'" + str 5 | .replace(/\t/g, "\\t") 6 | .replace(/\n/g, "\\n") 7 | .replace(/\r/g, "\\r") 8 | .replace(/'/g, "\\'") + "'" 9 | } 10 | 11 | function test(includeExampleData) { 12 | const testCases = [ 13 | { 14 | input: '{"SourceCode": "exampleDataHere"}', 15 | expected: 16 | 'type AutoGenerated struct {\n\tSourceCode string `json:"SourceCode"`\n}\n', 17 | expectedWithExample: 18 | 'type AutoGenerated struct {\n\tSourceCode string `json:"SourceCode" example:"exampleDataHere"`\n}\n', 19 | }, 20 | { 21 | input: '{"source_code": "exampleDataHere"}', 22 | expected: 23 | 'type AutoGenerated struct {\n\tSourceCode string `json:"source_code"`\n}\n', 24 | expectedWithExample: 25 | 'type AutoGenerated struct {\n\tSourceCode string `json:"source_code" example:"exampleDataHere"`\n}\n', 26 | }, 27 | { 28 | input: '{"sourceCode": "exampleDataHere"}', 29 | expected: 30 | 'type AutoGenerated struct {\n\tSourceCode string `json:"sourceCode"`\n}\n', 31 | expectedWithExample: 32 | 'type AutoGenerated struct {\n\tSourceCode string `json:"sourceCode" example:"exampleDataHere"`\n}\n', 33 | }, 34 | { 35 | input: '{"SOURCE_CODE": ""}', 36 | expected: 37 | 'type AutoGenerated struct {\n\tSourceCode string `json:"SOURCE_CODE"`\n}\n', 38 | expectedWithExample: 39 | 'type AutoGenerated struct {\n\tSourceCode string `json:"SOURCE_CODE"`\n}\n', 40 | }, 41 | { 42 | input: '{"PublicIP": ""}', 43 | expected: 44 | 'type AutoGenerated struct {\n\tPublicIP string `json:"PublicIP"`\n}\n', 45 | expectedWithExample: 46 | 'type AutoGenerated struct {\n\tPublicIP string `json:"PublicIP"`\n}\n', 47 | }, 48 | { 49 | input: '{"public_ip": ""}', 50 | expected: 51 | 'type AutoGenerated struct {\n\tPublicIP string `json:"public_ip"`\n}\n', 52 | expectedWithExample: 53 | 'type AutoGenerated struct {\n\tPublicIP string `json:"public_ip"`\n}\n', 54 | }, 55 | { 56 | input: '{"publicIP": ""}', 57 | expected: 58 | 'type AutoGenerated struct {\n\tPublicIP string `json:"publicIP"`\n}\n', 59 | expectedWithExample: 60 | 'type AutoGenerated struct {\n\tPublicIP string `json:"publicIP"`\n}\n', 61 | }, 62 | { 63 | input: '{"PUBLIC_IP": ""}', 64 | expected: 65 | 'type AutoGenerated struct {\n\tPublicIP string `json:"PUBLIC_IP"`\n}\n', 66 | expectedWithExample: 67 | 'type AutoGenerated struct {\n\tPublicIP string `json:"PUBLIC_IP"`\n}\n', 68 | }, 69 | { 70 | input: '{"+1": "Fails", "-1": "This should not cause duplicate field name"}', 71 | expected: 72 | 'type AutoGenerated struct {\n\tNum1 string `json:"+1"`\n\tNum10 string `json:"-1"`\n}\n', 73 | expectedWithExample: 74 | 'type AutoGenerated struct {\n\tNum1 string `json:"+1" example:"Fails"`\n\tNum10 string `json:"-1" example:"This should not cause duplicate field name"`\n}\n', 75 | }, 76 | { 77 | input: '{"age": 46}', 78 | expected: 79 | 'type AutoGenerated struct {\n\tAge int `json:"age"`\n}\n', 80 | expectedWithExample: 81 | 'type AutoGenerated struct {\n\tAge int `json:"age" example:"46"`\n}\n', 82 | }, 83 | { 84 | input: '{"negativeFloat": -1.00}', 85 | expected: 86 | 'type AutoGenerated struct {\n\tNegativeFloat float64 `json:"negativeFloat"`\n}\n', 87 | expectedWithExample: 88 | 'type AutoGenerated struct {\n\tNegativeFloat float64 `json:"negativeFloat" example:"-1.1"`\n}\n', 89 | }, 90 | { 91 | input: '{"zeroFloat": 0.00}', 92 | expected: 93 | 'type AutoGenerated struct {\n\tZeroFloat float64 `json:"zeroFloat"`\n}\n', 94 | expectedWithExample: 95 | 'type AutoGenerated struct {\n\tZeroFloat float64 `json:"zeroFloat" example:"0.1"`\n}\n', 96 | }, 97 | { 98 | input: '{"positiveFloat": 1.00}', 99 | expected: 100 | 'type AutoGenerated struct {\n\tPositiveFloat float64 `json:"positiveFloat"`\n}\n', 101 | expectedWithExample: 102 | 'type AutoGenerated struct {\n\tPositiveFloat float64 `json:"positiveFloat" example:"1.1"`\n}\n', 103 | }, 104 | { 105 | input: '{"negativeFloats": [-1.00, -2.00, -3.00]}', 106 | expected: 107 | 'type AutoGenerated struct {\n\tNegativeFloats []float64 `json:"negativeFloats"`\n}\n', 108 | expectedWithExample: 109 | 'type AutoGenerated struct {\n\tNegativeFloats []float64 `json:"negativeFloats"`\n}\n', 110 | }, 111 | { 112 | input: '{"zeroFloats": [0.00, 0.00, 0.00]}', 113 | expected: 114 | 'type AutoGenerated struct {\n\tZeroFloats []float64 `json:"zeroFloats"`\n}\n', 115 | expectedWithExample: 116 | 'type AutoGenerated struct {\n\tZeroFloats []float64 `json:"zeroFloats"`\n}\n', 117 | }, 118 | { 119 | input: '{"positiveFloats": [1.00, 2.00, 3.00]}', 120 | expected: 121 | 'type AutoGenerated struct {\n\tPositiveFloats []float64 `json:"positiveFloats"`\n}\n', 122 | expectedWithExample: 123 | 'type AutoGenerated struct {\n\tPositiveFloats []float64 `json:"positiveFloats"`\n}\n', 124 | }, 125 | { 126 | input: '{"topLevel": { "secondLevel": "exampleDataHere"} }', 127 | expected: 128 | 'type AutoGenerated struct {\n\tTopLevel struct {\n\t\tSecondLevel string `json:"secondLevel"`\n\t} `json:"topLevel"`\n}\n', 129 | expectedWithExample: 130 | 'type AutoGenerated struct {\n\tTopLevel struct {\n\t\tSecondLevel string `json:"secondLevel" example:"exampleDataHere"`\n\t} `json:"topLevel"`\n}\n', 131 | }, 132 | { 133 | input: '{"people": [{ "name": "Frank"}, {"name": "Dennis"}, {"name": "Dee"}, {"name": "Charley"}, {"name":"Mac"}] }', 134 | expected: 135 | 'type AutoGenerated struct {\n\tPeople []struct {\n\t\tName string `json:"name"`\n\t} `json:"people"`\n}\n', 136 | expectedWithExample: 137 | 'type AutoGenerated struct {\n\tPeople []struct {\n\t\tName string `json:"name" example:"Frank"`\n\t} `json:"people"`\n}\n', 138 | }, 139 | ]; 140 | 141 | for (const testCase of testCases) { 142 | const got = jsonToGo(testCase.input, null, null, includeExampleData); 143 | if (got.error) { 144 | console.assert(!got.error, `format('${testCase.input}'): ${got.error}`); 145 | process.exitCode = 16 146 | } else { 147 | const exp = includeExampleData ? testCase.expectedWithExample : testCase.expected 148 | const success = got.go === exp 149 | console.assert(success, 150 | `format('${testCase.input}'): \n\tgot: ${quote(got.go)}\n\twant: ${quote(exp)}` 151 | ); 152 | if(!success) process.exitCode = 17 153 | } 154 | } 155 | console.log(includeExampleData ? "done testing samples with data" : "done testing samples without data") 156 | } 157 | 158 | function testFiles() { 159 | const fs = require('fs'); 160 | const path = require('path'); 161 | 162 | const testCases = [ 163 | "duplicate-top-level-structs", 164 | "double-nested-objects", 165 | "array-with-mixed-float-int", 166 | "array-with-nonmatching-types", 167 | ]; 168 | 169 | for (const testCase of testCases) { 170 | 171 | try { 172 | const jsonData = fs.readFileSync(path.join('tests', testCase + '.json'), 'utf8'); 173 | const expectedGoData = fs.readFileSync(path.join('tests', testCase + '.go'), 'utf8'); 174 | const got = jsonToGo(jsonData); 175 | if (got.error) { 176 | console.assert(!got.error, `format('${jsonData}'): ${got.error}`); 177 | process.exitCode = 18 178 | } else { 179 | const success = got.go === expectedGoData 180 | console.assert(success, 181 | `format('${jsonData}'): \n\tgot: ${quote(got.go)}\n\twant: ${quote(expectedGoData)}` 182 | ); 183 | if(!success) process.exitCode = 19 184 | } 185 | } catch (err) { 186 | console.error(err); 187 | process.exitCode = 20 188 | } 189 | } 190 | console.log("done testing files") 191 | } 192 | 193 | test(false); 194 | test(true) 195 | testFiles() 196 | -------------------------------------------------------------------------------- /json-to-go.js: -------------------------------------------------------------------------------- 1 | /* 2 | JSON-to-Go 3 | by Matt Holt 4 | 5 | https://github.com/mholt/json-to-go 6 | 7 | A simple utility to translate JSON into a Go type definition. 8 | */ 9 | 10 | function jsonToGo(json, typename, flatten = true, example = false, allOmitempty = false) 11 | { 12 | let data; 13 | let scope; 14 | let go = ""; 15 | let tabs = 0; 16 | 17 | const seen = {}; 18 | const stack = []; 19 | let accumulator = ""; 20 | let innerTabs = 0; 21 | let parent = ""; 22 | let globallySeenTypeNames = []; 23 | let previousParents = ""; 24 | 25 | try 26 | { 27 | data = JSON.parse(json.replace(/(:\s*\[?\s*-?\d*)\.0/g, "$1.1")); // hack that forces floats to stay as floats 28 | scope = data; 29 | } 30 | catch (e) 31 | { 32 | return { 33 | go: "", 34 | error: e.message 35 | }; 36 | } 37 | 38 | typename = format(typename || "AutoGenerated"); 39 | append(`type ${typename} `); 40 | 41 | parseScope(scope); 42 | 43 | if (flatten) 44 | go += accumulator 45 | 46 | // add final newline for POSIX 3.206 47 | if (!go.endsWith(`\n`)) 48 | go += `\n` 49 | 50 | return { 51 | go: go 52 | }; 53 | 54 | 55 | function parseScope(scope, depth = 0) 56 | { 57 | if (typeof scope === "object" && scope !== null) 58 | { 59 | if (Array.isArray(scope)) 60 | { 61 | let sliceType; 62 | const scopeLength = scope.length; 63 | 64 | for (let i = 0; i < scopeLength; i++) 65 | { 66 | const thisType = goType(scope[i]); 67 | if (!sliceType) 68 | sliceType = thisType; 69 | else if (sliceType != thisType) 70 | { 71 | sliceType = mostSpecificPossibleGoType(thisType, sliceType); 72 | if (sliceType == "any") 73 | break; 74 | } 75 | } 76 | 77 | const slice = flatten && ["struct", "slice"].includes(sliceType) 78 | ? `[]${parent}` 79 | : `[]`; 80 | 81 | if (flatten && depth >= 2) 82 | appender(slice); 83 | else 84 | append(slice) 85 | if (sliceType == "struct") { 86 | const allFields = {}; 87 | 88 | // for each field counts how many times appears 89 | for (let i = 0; i < scopeLength; i++) 90 | { 91 | const keys = Object.keys(scope[i]) 92 | for (let k in keys) 93 | { 94 | let keyname = keys[k]; 95 | if (!(keyname in allFields)) { 96 | allFields[keyname] = { 97 | value: scope[i][keyname], 98 | count: 0 99 | } 100 | } 101 | else { 102 | const existingValue = allFields[keyname].value; 103 | const currentValue = scope[i][keyname]; 104 | 105 | if (!areSameType(existingValue, currentValue)) { 106 | if(existingValue !== null) { 107 | allFields[keyname].value = null // force type "any" if types are not identical 108 | console.warn(`Warning: key "${keyname}" uses multiple types. Defaulting to type "any".`) 109 | } 110 | allFields[keyname].count++ 111 | continue 112 | } 113 | 114 | // if variable was first detected as int (7) and a second time as float64 (3.14) 115 | // then we want to select float64, not int. Similar for int64 and float64. 116 | if(areSameType(currentValue, 1)) 117 | allFields[keyname].value = findBestValueForNumberType(existingValue, currentValue); 118 | 119 | if (areObjects(existingValue, currentValue)) { 120 | const comparisonResult = compareObjectKeys( 121 | Object.keys(currentValue), 122 | Object.keys(existingValue) 123 | ) 124 | if (!comparisonResult) { 125 | keyname = `${keyname}_${uuidv4()}`; 126 | allFields[keyname] = { 127 | value: currentValue, 128 | count: 0 129 | }; 130 | } 131 | } 132 | } 133 | allFields[keyname].count++; 134 | } 135 | } 136 | 137 | // create a common struct with all fields found in the current array 138 | // omitempty dict indicates if a field is optional 139 | const keys = Object.keys(allFields), struct = {}, omitempty = {}; 140 | for (let k in keys) 141 | { 142 | const keyname = keys[k], elem = allFields[keyname]; 143 | 144 | struct[keyname] = elem.value; 145 | omitempty[keyname] = elem.count != scopeLength; 146 | } 147 | parseStruct(depth + 1, innerTabs, struct, omitempty, previousParents); // finally parse the struct !! 148 | } 149 | else if (sliceType == "slice") { 150 | parseScope(scope[0], depth) 151 | } 152 | else { 153 | if (flatten && depth >= 2) { 154 | appender(sliceType || "any"); 155 | } else { 156 | append(sliceType || "any"); 157 | } 158 | } 159 | } 160 | else 161 | { 162 | if (flatten) { 163 | if (depth >= 2){ 164 | appender(parent) 165 | } 166 | else { 167 | append(parent) 168 | } 169 | } 170 | parseStruct(depth + 1, innerTabs, scope, false, previousParents); 171 | } 172 | } 173 | else { 174 | if (flatten && depth >= 2){ 175 | appender(goType(scope)); 176 | } 177 | else { 178 | append(goType(scope)); 179 | } 180 | } 181 | } 182 | 183 | function parseStruct(depth, innerTabs, scope, omitempty, oldParents) 184 | { 185 | if (flatten) { 186 | stack.push( 187 | depth >= 2 188 | ? "\n" 189 | : "" 190 | ) 191 | } 192 | 193 | const seenTypeNames = []; 194 | 195 | if (flatten && depth >= 2) 196 | { 197 | const parentType = `type ${parent}`; 198 | const scopeKeys = formatScopeKeys(Object.keys(scope)); 199 | 200 | // this can only handle two duplicate items 201 | // future improvement will handle the case where there could 202 | // three or more duplicate keys with different values 203 | if (parent in seen && compareObjectKeys(scopeKeys, seen[parent])) { 204 | stack.pop(); 205 | return 206 | } 207 | seen[parent] = scopeKeys; 208 | 209 | appender(`${parentType} struct {\n`); 210 | ++innerTabs; 211 | const keys = Object.keys(scope); 212 | previousParents = parent 213 | for (let i in keys) 214 | { 215 | const keyname = getOriginalName(keys[i]); 216 | indenter(innerTabs) 217 | let typename 218 | // structs will be defined on the top level of the go file, so they need to be globally unique 219 | if (typeof scope[keys[i]] === "object" && scope[keys[i]] !== null) { 220 | typename = uniqueTypeName(format(keyname), globallySeenTypeNames, previousParents) 221 | globallySeenTypeNames.push(typename) 222 | } else { 223 | typename = uniqueTypeName(format(keyname), seenTypeNames) 224 | seenTypeNames.push(typename) 225 | } 226 | 227 | appender(typename+" "); 228 | parent = typename 229 | parseScope(scope[keys[i]], depth); 230 | appender(' `json:"'+keyname); 231 | if (allOmitempty || (omitempty && omitempty[keys[i]] === true)) 232 | { 233 | appender(',omitempty'); 234 | } 235 | appender('"`\n'); 236 | } 237 | indenter(--innerTabs); 238 | appender("}"); 239 | previousParents = oldParents; 240 | } 241 | else 242 | { 243 | append("struct {\n"); 244 | ++tabs; 245 | const keys = Object.keys(scope); 246 | previousParents = parent 247 | for (let i in keys) 248 | { 249 | const keyname = getOriginalName(keys[i]); 250 | indent(tabs); 251 | let typename 252 | // structs will be defined on the top level of the go file, so they need to be globally unique 253 | if (typeof scope[keys[i]] === "object" && scope[keys[i]] !== null) { 254 | typename = uniqueTypeName(format(keyname), globallySeenTypeNames, previousParents) 255 | globallySeenTypeNames.push(typename) 256 | } else { 257 | typename = uniqueTypeName(format(keyname), seenTypeNames) 258 | seenTypeNames.push(typename) 259 | } 260 | 261 | append(typename+" "); 262 | parent = typename 263 | parseScope(scope[keys[i]], depth); 264 | append(' `json:"'+keyname); 265 | if (allOmitempty || (omitempty && omitempty[keys[i]] === true)) 266 | { 267 | append(',omitempty'); 268 | } 269 | if (example && scope[keys[i]] !== "" && typeof scope[keys[i]] !== "object") 270 | { 271 | append('" example:"'+scope[keys[i]]) 272 | } 273 | append('"`\n'); 274 | } 275 | indent(--tabs); 276 | append("}"); 277 | previousParents = oldParents; 278 | } 279 | if (flatten) 280 | accumulator += stack.pop(); 281 | } 282 | 283 | function indent(tabs) 284 | { 285 | for (let i = 0; i < tabs; i++) 286 | go += '\t'; 287 | } 288 | 289 | function append(str) 290 | { 291 | go += str; 292 | } 293 | 294 | function indenter(tabs) 295 | { 296 | for (let i = 0; i < tabs; i++) 297 | stack[stack.length - 1] += '\t'; 298 | } 299 | 300 | function appender(str) 301 | { 302 | stack[stack.length - 1] += str; 303 | } 304 | 305 | // Generate a unique name to avoid duplicate struct field names. 306 | // This function appends a number at the end of the field name. 307 | function uniqueTypeName(name, seen, prefix=null) { 308 | if (seen.indexOf(name) === -1) { 309 | return name; 310 | } 311 | 312 | // check if we can get a unique name by prefixing it 313 | if(prefix) { 314 | name = prefix+name 315 | if (seen.indexOf(name) === -1) { 316 | return name; 317 | } 318 | } 319 | 320 | let i = 0; 321 | while (true) { 322 | let newName = name + i.toString(); 323 | if (seen.indexOf(newName) === -1) { 324 | return newName; 325 | } 326 | 327 | i++; 328 | } 329 | } 330 | 331 | // Sanitizes and formats a string to make an appropriate identifier in Go 332 | function format(str) 333 | { 334 | str = formatNumber(str); 335 | 336 | let sanitized = toProperCase(str).replace(/[^a-z0-9]/ig, "") 337 | if (!sanitized) { 338 | return "NAMING_FAILED"; 339 | } 340 | 341 | // After sanitizing the remaining characters can start with a number. 342 | // Run the sanitized string again trough formatNumber to make sure the identifier is Num[0-9] or Zero_... instead of 1. 343 | return formatNumber(sanitized) 344 | } 345 | 346 | // Adds a prefix to a number to make an appropriate identifier in Go 347 | function formatNumber(str) { 348 | if (!str) 349 | return ""; 350 | else if (str.match(/^\d+$/)) 351 | str = "Num" + str; 352 | else if (str.charAt(0).match(/\d/)) 353 | { 354 | const numbers = {'0': "Zero_", '1': "One_", '2': "Two_", '3': "Three_", 355 | '4': "Four_", '5': "Five_", '6': "Six_", '7': "Seven_", 356 | '8': "Eight_", '9': "Nine_"}; 357 | str = numbers[str.charAt(0)] + str.substr(1); 358 | } 359 | 360 | return str; 361 | } 362 | 363 | // Determines the most appropriate Go type 364 | function goType(val) 365 | { 366 | if (val === null) 367 | return "any"; 368 | 369 | switch (typeof val) 370 | { 371 | case "string": 372 | if (/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(\+\d\d:\d\d|Z)$/.test(val)) 373 | return "time.Time"; 374 | else 375 | return "string"; 376 | case "number": 377 | if (val % 1 === 0) 378 | { 379 | if (val > -2147483648 && val < 2147483647) 380 | return "int"; 381 | else 382 | return "int64"; 383 | } 384 | else 385 | return "float64"; 386 | case "boolean": 387 | return "bool"; 388 | case "object": 389 | if (Array.isArray(val)) 390 | return "slice"; 391 | return "struct"; 392 | default: 393 | return "any"; 394 | } 395 | } 396 | 397 | // change the value to expand ints and floats to their larger equivalent 398 | function findBestValueForNumberType(existingValue, newValue) { 399 | if (!areSameType(newValue, 1)) { 400 | console.error(`Error: currentValue ${newValue} is not a number`) 401 | return null // falls back to goType "any" 402 | } 403 | 404 | const newGoType = goType(newValue) 405 | const existingGoType = goType(existingValue) 406 | 407 | if (newGoType === existingGoType) 408 | return existingValue 409 | 410 | // always upgrade float64 411 | if (newGoType === "float64") 412 | return newValue 413 | if (existingGoType === "float64") 414 | return existingValue 415 | 416 | // it's too complex to distinguish int types and float32, so we force-upgrade to float64 417 | // if anyone has a better suggestion, PRs are welcome! 418 | if (newGoType.includes("float") && existingGoType.includes("int")) 419 | return Number.MAX_VALUE 420 | if (newGoType.includes("int") && existingGoType.includes("float")) 421 | return Number.MAX_VALUE 422 | 423 | if (newGoType.includes("int") && existingGoType.includes("int")) { 424 | const existingValueAbs = Math.abs(existingValue); 425 | const newValueAbs = Math.abs(newValue); 426 | 427 | // if the sum is overflowing, it's safe to assume numbers are very large. So we force int64. 428 | if (!isFinite(existingValueAbs + newValueAbs)) 429 | return Number.MAX_SAFE_INTEGER 430 | 431 | // it's too complex to distinguish int8, int16, int32 and int64, so we just use the sum as best-guess 432 | return existingValueAbs + newValueAbs; 433 | } 434 | 435 | // There should be other cases 436 | console.error(`Error: something went wrong with findBestValueForNumberType() using the values: '${newValue}' and '${existingValue}'`) 437 | console.error(" Please report the problem to https://github.com/mholt/json-to-go/issues") 438 | return null // falls back to goType "any" 439 | } 440 | 441 | // Given two types, returns the more specific of the two 442 | function mostSpecificPossibleGoType(typ1, typ2) 443 | { 444 | if (typ1.substr(0, 5) == "float" 445 | && typ2.substr(0, 3) == "int") 446 | return typ1; 447 | else if (typ1.substr(0, 3) == "int" 448 | && typ2.substr(0, 5) == "float") 449 | return typ2; 450 | else 451 | return "any"; 452 | } 453 | 454 | // Proper cases a string according to Go conventions 455 | function toProperCase(str) 456 | { 457 | // ensure that the SCREAMING_SNAKE_CASE is converted to snake_case 458 | if (str.match(/^[_A-Z0-9]+$/)) { 459 | str = str.toLowerCase(); 460 | } 461 | 462 | // https://github.com/golang/lint/blob/5614ed5bae6fb75893070bdc0996a68765fdd275/lint.go#L771-L810 463 | const commonInitialisms = [ 464 | "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", 465 | "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", 466 | "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID", 467 | "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" 468 | ]; 469 | 470 | return str.replace(/(^|[^a-zA-Z])([a-z]+)/g, function(unused, sep, frag) 471 | { 472 | if (commonInitialisms.indexOf(frag.toUpperCase()) >= 0) 473 | return sep + frag.toUpperCase(); 474 | else 475 | return sep + frag[0].toUpperCase() + frag.substr(1).toLowerCase(); 476 | }).replace(/([A-Z])([a-z]+)/g, function(unused, sep, frag) 477 | { 478 | if (commonInitialisms.indexOf(sep + frag.toUpperCase()) >= 0) 479 | return (sep + frag).toUpperCase(); 480 | else 481 | return sep + frag; 482 | }); 483 | } 484 | 485 | function uuidv4() { 486 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 487 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 488 | return v.toString(16); 489 | }); 490 | } 491 | 492 | function getOriginalName(unique) { 493 | 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 494 | const uuidLength = 36; 495 | 496 | if (unique.length >= uuidLength) { 497 | const tail = unique.substr(-uuidLength); 498 | if (reLiteralUUID.test(tail)) { 499 | return unique.slice(0, -1 * (uuidLength + 1)) 500 | } 501 | } 502 | return unique 503 | } 504 | 505 | function areObjects(objectA, objectB) { 506 | const object = "[object Object]"; 507 | return Object.prototype.toString.call(objectA) === object 508 | && Object.prototype.toString.call(objectB) === object; 509 | } 510 | 511 | function areSameType(objectA, objectB) { 512 | // prototype.toString required to compare Arrays and Objects 513 | const typeA = Object.prototype.toString.call(objectA) 514 | const typeB = Object.prototype.toString.call(objectB) 515 | return typeA === typeB 516 | } 517 | 518 | function compareObjectKeys(itemAKeys, itemBKeys) { 519 | const lengthA = itemAKeys.length; 520 | const lengthB = itemBKeys.length; 521 | 522 | // nothing to compare, probably identical 523 | if (lengthA == 0 && lengthB == 0) 524 | return true; 525 | 526 | // duh 527 | if (lengthA != lengthB) 528 | return false; 529 | 530 | for (let item of itemAKeys) { 531 | if (!itemBKeys.includes(item)) 532 | return false; 533 | } 534 | return true; 535 | } 536 | 537 | function formatScopeKeys(keys) { 538 | for (let i in keys) { 539 | keys[i] = format(keys[i]); 540 | } 541 | return keys 542 | } 543 | } 544 | 545 | if (typeof module != 'undefined') { 546 | if (!module.parent) { 547 | let filename = null 548 | 549 | function jsonToGoWithErrorHandling(json) { 550 | const output = jsonToGo(json) 551 | if (output.error) { 552 | console.error(output.error) 553 | process.exitCode = 1 554 | } 555 | process.stdout.write(output.go) 556 | } 557 | 558 | process.argv.forEach((val, index) => { 559 | if (index < 2) 560 | return 561 | 562 | if (!val.startsWith('-')) { 563 | filename = val 564 | return 565 | } 566 | 567 | const argument = val.replace(/-/g, '') 568 | if (argument === "big") 569 | console.warn(`Warning: The argument '${argument}' has been deprecated and has no effect anymore`) 570 | else { 571 | console.error(`Unexpected argument ${val} received`) 572 | process.exit(1) 573 | } 574 | }) 575 | 576 | if (filename) { 577 | const fs = require('fs'); 578 | const json = fs.readFileSync(filename, 'utf8'); 579 | jsonToGoWithErrorHandling(json) 580 | return 581 | } 582 | 583 | if (!filename) { 584 | bufs = [] 585 | process.stdin.on('data', function(buf) { 586 | bufs.push(buf) 587 | }) 588 | process.stdin.on('end', function() { 589 | const json = Buffer.concat(bufs).toString('utf8') 590 | jsonToGoWithErrorHandling(json) 591 | }) 592 | return 593 | } 594 | } else { 595 | module.exports = jsonToGo 596 | } 597 | } 598 | --------------------------------------------------------------------------------