package main import ( "encoding/json" "flag" "fmt" "log" "os" "reflect" "strings" "time" "unicode/utf8" "github.com/iancoleman/orderedmap" "github.com/xuri/excelize/v2" ) // Initial concept from https://stackoverflow.com/q/68621039 type Config struct { keyOrder map[int]string parentOverride string } var config Config func main() { //jsonFile := "test.json" parentNode := "input" configNode := "config" //worksheetName := "Sheet2" //outputFilename := "test.xlsx" // Command line arguments var inputJson string var worksheetName string var outputFilename string var boldTopRow bool var freezeTopRow bool var autoFilter bool var autoWidth bool var overwriteFile bool // Process command line arguments flag.StringVar(&inputJson, "inputJson", "./input.json", "Full path to input json data file") flag.StringVar(&outputFilename, "outputFilename", "./output.xlsx", "Filename for excel worksheet output") flag.StringVar(&worksheetName, "worksheetName", "Sheet1", "Label to set on worksheet") flag.BoolVar(&boldTopRow, "bold-toprow", true, "Sets the top row of the worksheet to bold") flag.BoolVar(&freezeTopRow, "freeze-toprow", true, "Freezes the first row of the Excel worksheet") flag.BoolVar(&autoFilter, "autofilter", true, "Sets the auto filter on the first row") flag.BoolVar(&autoWidth, "autowidth", true, "Automatically set the column width to fit contents") flag.BoolVar(&overwriteFile, "overwrite", false, "Set to true to overwrite existing file rather than modifying in place") flag.Parse() var xlsx *excelize.File var s []byte //var sheetIndex int var err error var cell string var row, column int // Truncate worksheetName to the maximum 31 characters worksheetName = TruncateString(worksheetName, 31) // Read the json input file if fileExists(inputJson) { s, err = os.ReadFile(inputJson) if err != nil { panic(err) } } else { fmt.Printf("Input JSON file '%s' does not exist.\n", inputJson) os.Exit(1) } // Unmarshal the json into an orderedmap to preserve the ordering of json structure o := orderedmap.New() err = json.Unmarshal([]byte(s), &o) if err != nil { error := fmt.Sprintf("JSON Unmarshal error %s\n", err) panic(error) } // Assume that our content is within the first top-level key topLevel := o.Keys() fmt.Printf("Found %d top-level keys in json data\n", len(topLevel)) for i, key := range topLevel { fmt.Printf("[%d] : %s\n", i, key) } // Check for config embedded in json if strings.EqualFold(topLevel[0], configNode) && len(topLevel) > 1 { fmt.Printf("Found configNode as toplevel json key, setting parentNode as '%s'\n", topLevel[1]) parentNode = topLevel[1] config.keyOrder = make(map[int]string) // Get a reference to the top level node we specified earlier configInterface, ok := o.Get(configNode) if !ok { fmt.Printf("Missing key for multitype array when reading embedded config") } // Get an interface that we can work with to access the sub elements // This doesn't seem necessary for some reason - maybe because there's only one level of depth to the configNode //configSlice := configInterface //fmt.Printf("%v\n", configSlice) // Get the keys for the first element so we know what config options have been specified configMap := configInterface.(orderedmap.OrderedMap) configKeys := configMap.Keys() // Parse each key into our config struct for _, key := range configKeys { if strings.EqualFold(key, "keyOrder") { fmt.Printf("Found config element for keyOrder\n") e, _ := configMap.Get(key) for i, e := range strings.Split(e.(string), ",") { config.keyOrder[i] = e } fmt.Printf("Column order is now : '%v'\n", config.keyOrder) } else if strings.EqualFold(key, "overwriteFile") { fmt.Printf("Found config element for overwriting output file\n") e, _ := configMap.Get(key) overwriteFile = e.(bool) } else if strings.EqualFold(key, "parentNode") { fmt.Printf("Found config element for forcing parent key for spreadsheet data\n") e, _ := configMap.Get(key) config.parentOverride = e.(string) } } } else if strings.EqualFold(topLevel[0], configNode) { error := "Only found config in first level of json keys" panic(error) } else { fmt.Printf("Detected toplevel json key as: '%s'\n", topLevel[0]) parentNode = topLevel[0] } if config.parentOverride != "" { fmt.Printf("Overriding parent node to '%s'\n", config.parentOverride) parentNode = config.parentOverride } // Get a reference to the top level node we specified earlier vislice, ok := o.Get(parentNode) if !ok { fmt.Printf("Missing key for multitype array") } // Get an interface that we can work with to access the sub elements vslice := vislice.([]interface{}) if len(vslice) < 1 { // There was no data but lets just log that and close off the empty workbook fmt.Printf("No data found contained in top-level json key '%s', no work to do.\n", parentNode) // Close off the file if err := xlsx.SaveAs(outputFilename); err != nil { log.Fatal(err) } return } // Check that the first element is what we expected if _, ok := vslice[0].(orderedmap.OrderedMap); !ok { error := fmt.Sprintf("Type of first vslice element is not an ordered map. It appears to be '%v'\n", reflect.TypeOf(vslice[0])) panic(error) } // Get the keys for the first element so we know what the column names will be columnMap := vslice[0].(orderedmap.OrderedMap) //fmt.Printf("First vslice element is an ordered map\n") columnNames := columnMap.Keys() // Check if xlsx file exists already, and if it does then open and append data if fileExists(outputFilename) { if overwriteFile { // Delete existing file fmt.Printf("Overwrite flag is set, removing existing file '%s'\n", outputFilename) err = os.Remove(outputFilename) if err != nil { panic(err) } // Create new file xlsx = createWorkbook(worksheetName, outputFilename) } else { xlsx = modifyWorkbook(worksheetName, outputFilename) } } else { xlsx = createWorkbook(worksheetName, outputFilename) } // Run code to add column names to the first row of the workbook createHeadingRow(xlsx, worksheetName, columnNames) // Set the style for the header values // Just handling bold for now but we can do other styles too as per https://xuri.me/excelize/en/style.html#NewStyle fmt.Printf("Bolding top row : %v\n", boldTopRow) headerStyle, err2 := xlsx.NewStyle(&excelize.Style{ Font: &excelize.Font{ Bold: true, }, }) if err2 != nil { fmt.Printf("Error generating header style : '%s'\n", err2) } // Set the style if boldTopRow { err = xlsx.SetRowStyle(worksheetName, 1, 1, headerStyle) if err != nil { fmt.Printf("Error setting header style : '%s'\n", err) } } // Freeze top row if requested, see https://xuri.me/excelize/en/utils.html#SetPanes if freezeTopRow { err = xlsx.SetPanes(worksheetName, &excelize.Panes{ Freeze: true, Split: false, XSplit: 0, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft", Panes: []excelize.PaneOptions{ {SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"}, }, }) if err != nil { fmt.Printf("Error freezing top row : '%s'\n", err) } } // Handle autofilter if autoFilter { cell, _ = excelize.CoordinatesToCellName(len(columnNames), 1) filterRange := "A1:" + cell fmt.Printf("Setting autofilter to range '%s'\n", filterRange) // As per docs any filters applied need to be manually processed by us (eg hiding rows with blanks) err = xlsx.AutoFilter(worksheetName, filterRange, nil) if err != nil { fmt.Printf("Error setting autofilter : '%s'\n", err) } } // Now process the remaining data in our json input // Set starting row for data row = 2 // Iterate the whole slice to get the data and add to the worksheet fmt.Printf("Adding %d rows of data to spreadsheet.\n", len(vslice)) for i, v := range vslice { // Print the contents each slice //fmt.Printf("'%d' : '%v'\n", i, v) //fmt.Printf("Slice element %d\n", i) _ = i // Each iteration should start back at column 1 column = 1 // Get the key-value pairs contained in this slice vmap := v.(orderedmap.OrderedMap) k := vmap.Keys() if len(config.keyOrder) > 0 { // If we have a specified order for the values then use that for j := 0; j < len(config.keyOrder); j++ { cell, _ = excelize.CoordinatesToCellName(column, row) e, _ := vmap.Get(config.keyOrder[j]) //fmt.Printf("Setting cell %s to value %v\n", cell, e) xlsx.SetCellValue(worksheetName, cell, e) // Move to the next column column++ } } else { // Otherwise use the order the json was in for j := range k { cell, _ = excelize.CoordinatesToCellName(column, row) e, _ := vmap.Get(k[j]) //fmt.Printf("Setting cell %s to value %v\n", cell, e) xlsx.SetCellValue(worksheetName, cell, e) // Move to the next column column++ } } //fmt.Printf("k: %v\n", k) // Move to next row row++ } // Perform any post processing now that the data exists if autoWidth { err = SetColAutoWidth(xlsx, worksheetName) if err != nil { fmt.Printf("Error setting auto width : '%s'\n", err) } } // Close off the file if err := xlsx.SaveAs(outputFilename); err != nil { log.Fatal(err) return } fmt.Printf("Excel file '%s' generated sucessfully.\n", outputFilename) /* //Test https://gitlab.com/c0b/go-ordered-json var om *ordered.OrderedMap = ordered.NewOrderedMap() err = json.Unmarshal([]byte(file), om) if err != nil { fmt.Printf("error: '%s'\n", err) } iter := om.EntriesIter() for { pair, ok := iter() if !ok { break } fmt.Printf("%-12s: %v\n", pair.Key, pair.Value) } */ } func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } // From https://dev.to/takakd/go-safe-truncate-string-9h0#comment-1hp14 func TruncateString(str string, length int) string { if length <= 0 { return "" } if utf8.RuneCountInString(str) < length { return str } return string([]rune(str)[:length]) } func createWorkbook(worksheetName string, outputFilename string) *excelize.File { fmt.Printf("Creating output spreadsheet '%s'\n", outputFilename) var err error // Create the file xlsx := excelize.NewFile() // Rename the default Sheet1 to this worksheet name if worksheetName != "Sheet1" { fmt.Printf("Renaming default worksheet to '%s'\n", worksheetName) err = xlsx.SetSheetName("Sheet1", worksheetName) if err != nil { fmt.Printf("Error setting sheet name to '%s': %s\n", worksheetName, err) } } // Set the document properties err = xlsx.SetDocProps(&excelize.DocProperties{ Creator: "json2excel", Created: time.Now().Format(time.RFC3339), }) if err != nil { fmt.Printf("Error setting document properties: %s\n", err) } return xlsx } func modifyWorkbook(worksheetName string, outputFilename string) *excelize.File { fmt.Printf("Output spreadsheet '%s' already exists.\n", outputFilename) xlsx, err := excelize.OpenFile(outputFilename) if err != nil { fmt.Println(err) os.Exit(1) } // Since we have an existing workbook, check if the sheet we want to write to already exists sheetFound := false for index, name := range xlsx.GetSheetMap() { if name == worksheetName { fmt.Printf("Found worksheet '%s' at index '%d'\n", worksheetName, index) sheetFound = true } } if !sheetFound { // Create the sheet fmt.Printf("Creating worksheet '%s'\n", worksheetName) sheetIndex, err := xlsx.NewSheet(worksheetName) if err != nil { fmt.Printf("Error creating worksheet '%s' : %s\n", worksheetName, err) } // Set active worksheet fmt.Printf("Setting active sheet to index %d", sheetIndex) xlsx.SetActiveSheet(sheetIndex) } return xlsx } func createHeadingRow(xlsx *excelize.File, worksheetName string, columnNames []string) { var cell string row := 1 column := 1 if len(config.keyOrder) > 0 { fmt.Printf("Creating excel workbook with heading order as per config key-order\n") // Check that the number of specified columns matches input data if len(config.keyOrder) != len(columnNames) { error := fmt.Sprintf("Column order specified in json key-order but mismatch found in json data. %d specifed columns does not match %d found columns.", len(config.keyOrder), len(columnNames)) panic(error) } // Iterate the map and add the columns as per that order for i := 0; i < len(config.keyOrder); i++ { cell, _ = excelize.CoordinatesToCellName(column, row) fmt.Printf("Setting cell %s to value %s at index %d\n", cell, config.keyOrder[i], i) xlsx.SetCellValue(worksheetName, cell, config.keyOrder[i]) column++ } } else { fmt.Printf("Creating excel workbook with following headings : '%v'\n", columnNames) // Add the header row for i := 0; i < len(columnNames); i++ { cell, _ = excelize.CoordinatesToCellName(column, row) fmt.Printf("Setting cell %s to value %s\n", cell, columnNames[i]) xlsx.SetCellValue(worksheetName, cell, columnNames[i]) //xlsx.SetCellStyle(worksheetName, cell, cell, headerStyle) column++ } } } // Taken from https://github.com/qax-os/excelize/issues/92#issuecomment-821578446 func SetColAutoWidth(xlsx *excelize.File, sheetName string) error { // Autofit all columns according to their text content cols, err := xlsx.GetCols(sheetName) if err != nil { return err } for idx, col := range cols { largestWidth := 0 for _, rowCell := range col { cellWidth := utf8.RuneCountInString(rowCell) + 2 // + 2 for margin if cellWidth > largestWidth { largestWidth = cellWidth } } fmt.Printf("SetColAutoWidth calculated largest width for column index '%d' is '%d'\n", idx, largestWidth) name, err := excelize.ColumnNumberToName(idx + 1) if err != nil { return err } xlsx.SetColWidth(sheetName, name, name, float64(largestWidth)) } // No errors at this point return nil } func TestUnmarshalJSON() { s := `{ "number": 4, "string": "x", "z": 1, "a": "should not break with unclosed { character in value", "b": 3, "slice": [ "1", 1 ], "orderedmap": { "e": 1, "a { nested key with brace": "with a }}}} }} {{{ brace value", "after": { "link": "test {{{ with even deeper nested braces }" } }, "test\"ing": 9, "after": 1, "multitype_array": [ "test", 1, { "map": "obj", "it" : 5, ":colon in key": "colon: in value" }, [{"inner": "map"}] ], "should not break with { character in key": 1 }` o := orderedmap.New() err := json.Unmarshal([]byte(s), &o) if err != nil { fmt.Printf("JSON Unmarshal error", err) } // Check the root keys expectedKeys := []string{ "number", "string", "z", "a", "b", "slice", "orderedmap", "test\"ing", "after", "multitype_array", "should not break with { character in key", } k := o.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Unmarshal root key order", i, k[i], "!=", expectedKeys[i]) } } // Check nested maps are converted to orderedmaps // nested 1 level deep expectedKeys = []string{ "e", "a { nested key with brace", "after", } vi, ok := o.Get("orderedmap") if !ok { fmt.Printf("Missing key for nested map 1 deep") } v := vi.(orderedmap.OrderedMap) k = v.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 1 deep ", i, k[i], "!=", expectedKeys[i]) } } // nested 2 levels deep expectedKeys = []string{ "link", } vi, ok = v.Get("after") if !ok { fmt.Printf("Missing key for nested map 2 deep") } v = vi.(orderedmap.OrderedMap) k = v.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 2 deep", i, k[i], "!=", expectedKeys[i]) } } // multitype array expectedKeys = []string{ "map", "it", ":colon in key", } vislice, ok := o.Get("multitype_array") if !ok { fmt.Printf("Missing key for multitype array") } vslice := vislice.([]interface{}) vmap := vslice[2].(orderedmap.OrderedMap) k = vmap.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 2 deep", i, k[i], "!=", expectedKeys[i]) } } // nested map 3 deep vislice, _ = o.Get("multitype_array") vslice = vislice.([]interface{}) expectedKeys = []string{"inner"} vinnerslice := vslice[3].([]interface{}) vinnermap := vinnerslice[0].(orderedmap.OrderedMap) k = vinnermap.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 3 deep", i, k[i], "!=", expectedKeys[i]) } } }