Creating a Code Generator
How often do we run into repetitive tasks that, because they are routine and trivial, we simply ignore? It is curious: some developers create aliases for tiny Git commands, yet keep losing precious minutes whenever they need to create a new use case.
You know when a component always requires an implementation of a specific interface? Or when the constructor for that new component invariably needs the same validations? Or when your favorite LLM decides to “help” by changing attributes with a single tab and only gets in the way? Sometimes, simple — when done well — can save a lot of our time.
What will we need?
- Blunt scissors – optional, but you never know.
- fmt – It is useful to display clear outputs when something fails.
- go/format – Our automatic Go code formatter.
- os – To manipulate/create the output folders for the files.
- path/filepath – To navigate easily among the source files.
- strings – To manipulate input and output text.
- text/template – The core of everything here. See the docs
- toml, env, conf – Any library for managing configuration; I used toml here, but feel free to adapt it.
Step by step
In the example below, I use the library to generate a set of Go files for the structures we will use in a server-driven UI system. The output JSON files are defined by the frontend team.
1. Defining structs and loading configuration
type Config struct {
InputFolder string `toml:"input-folder"`
OutputFolder string `toml:"output-folder"`
}
type JSONData struct {
UIType string `json:"ui_type"`
Data map[string]interface{} `json:"data"`
}
type Field struct {
Name string
Type string
JSONTag string
IsStruct bool
Struct *StructDef
}
type StructDef struct {
Name string
Fields []Field
}
func main() {
configData, err := os.ReadFile("conf-frog.toml")
if err != nil {
fmt.Fprintf(os.Stderr, "error reading configuration file: %v\n", err)
os.Exit(1)
}
var config Config
if err := toml.Unmarshal(configData, &config); err != nil {
fmt.Fprintf(os.Stderr, "error parsing TOML: %v\n", err)
os.Exit(1)
}
if config.InputFolder == "" {
fmt.Fprintf(os.Stderr, "error: 'input-folder' is not specified in the configuration\n")
os.Exit(1)
}
if config.OutputFolder == "" {
fmt.Fprintf(os.Stderr, "error: 'output-folder' is not specified in the configuration\n")
os.Exit(1)
}
// Create the output folder if it does not exist
if err := os.MkdirAll(config.OutputFolder, 0755); err != nil {
fmt.Fprintf(os.Stderr, "error creating output folder: %v\n", err)
os.Exit(1)
}
// Find all JSON files in the input folder
jsonFiles, err := filepath.Glob(filepath.Join(config.InputFolder, "*.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "error searching for JSON files: %v\n", err)
os.Exit(1)
}
if len(jsonFiles) == 0 {
fmt.Printf("No JSON files found in %s\n", config.InputFolder)
return
}
// Process each JSON file
for _, jsonFile := range jsonFiles {
if err := processJSONFile(jsonFile, config.OutputFolder); err != nil {
fmt.Fprintf(os.Stderr, "error processing %s: %v\n", jsonFile, err)
continue
}
fmt.Printf("Processado: %s\n", jsonFile)
}
}
2. Starting the processing
A good old loop to go through all the files in the folder defined as input
for _, jsonFile := range jsonFiles {
if err := processJSONFile(jsonFile, config.OutputFolder); err != nil {
fmt.Fprintf(os.Stderr, "error processing %s: %v\n", jsonFile, err)
continue
}
fmt.Printf("Processado: %s\n", jsonFile)
}
3. Breaking down processJSONFile
These first steps are just manipulations I made to make generating the next stage more practical
- Read the JSON file
- Unmarshal it into a structure
- Get the
.jsonfilename to use when creating the new file
// Read and parse JSON
data, err := os.ReadFile(jsonPath)
if err != nil {
return fmt.Errorf("error reading file: %w", err)
}
var jsonData JSONData
if err := json.Unmarshal(data, &jsonData); err != nil {
return fmt.Errorf("error parsing JSON: %w", err)
}
// Get the filename without its extension
baseName := filepath.Base(jsonPath)
fileName := strings.TrimSuffix(baseName, ".json")
// Generate the main struct (using the filename as a prefix for nested structs)
mainStruct := generateStruct(jsonData.UIType, jsonData.Data, "")
4. Manipulating
Manipulate everything necessary and possible to make file creation easier. Here I extract everything that can and must be used when creating the new structs.
type Field struct {
Name string
Type string
JSONTag string
IsStruct bool
Struct *StructDef
}
type StructDef struct {
Name string
Fields []Field
}
5. Reflection
This is shock therapy for anyone who avoids reflection at all costs. Enjoy the vibe and accept that this will occasionally go wrong, but everything works out in the end.
Jokes aside, we cannot avoid it: what we are doing is metaprogramming in its purest form. It is code generating code.
func generateStruct(name string, data map[string]interface{}, parentPrefix string) *StructDef {
// Struct name: if there is a parentPrefix, use parentPrefix + capitalize(name); otherwise, just capitalize(name)
structName := capitalize(name)
if parentPrefix != "" {
structName = parentPrefix + structName
}
structDef := &StructDef{
Name: structName,
Fields: []Field{},
}
for key, value := range data {
field := Field{
Name: capitalize(key),
JSONTag: key,
}
switch v := value.(type) {
case string:
field.Type = "string"
case float64:
// Check whether it is an integer
if v == float64(int64(v)) {
field.Type = "int"
} else {
field.Type = "float64"
}
case bool:
field.Type = "bool"
case []interface{}:
// Array - check the type of the first element
if len(v) > 0 {
elemType := inferType(v[0])
field.Type = "[]" + elemType
} else {
field.Type = "[]interface{}"
}
case map[string]interface{}:
// Nested struct - use the parent type as a prefix
field.IsStruct = true
field.Struct = generateStruct(key, v, structName)
field.Type = field.Struct.Name
default:
field.Type = "interface{}"
}
structDef.Fields = append(structDef.Fields, field)
}
return structDef
}
6. Behind the scenes
I kept this section as direct as possible to explain the details that were not covered, but I believe the function names make their purpose clear
// Uppercase so that the new structs are exported
func capitalize(s string) string {
if s == "" {
return s
}
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}
// How do we determine the type of each field in the JSON example supplied by the frontend?
func inferType(value interface{}) string {
switch v := value.(type) {
case string:
return "string"
case float64:
if v == float64(int64(v)) {
return "int"
}
return "float64"
case bool:
return "bool"
case map[string]interface{}:
return "map[string]interface{}"
default:
return "interface{}"
}
}
7. The star of the show
Here we reach the climax: the moment when the file is actually generated. Pay attention to collectStructs, which recursively ensures that objects can be nested and still produce valid code.
func generateGoCode(packageName string, mainStruct *StructDef) (string, error) {
tmpl := `// Code generated by frog; DO NOT EDIT.
package {{.Package}}
{{range .AllStructs}}
type {{.Name}} struct {
{{range .Fields}} {{.Name}} {{.Type}} ` + "`json:\"{{.JSONTag}}\"`" + `
{{end}}}
{{end}}
`
t, err := template.New("codegen").Parse(tmpl)
if err != nil {
return "", err
}
// Collect all structs (main + nested)
allStructs := collectAllStructs(mainStruct)
data := struct {
Package string
AllStructs []*StructDef
}{
Package: packageName,
AllStructs: allStructs,
}
var buf strings.Builder
if err := t.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
func collectAllStructs(structDef *StructDef) []*StructDef {
var all []*StructDef
// Add nested structs first
for _, field := range structDef.Fields {
if field.IsStruct && field.Struct != nil {
all = append(all, collectAllStructs(field.Struct)...)
}
}
// Add the main struct last
all = append(all, structDef)
return all
}
8. Cleaning up the mess
The moment when we make the linter and reviewers happy. This is the missing part of processJSONFile
// Format code
formatted, err := format.Source([]byte(code))
if err != nil {
return fmt.Errorf("error formatting code: %w", err)
}
// Create the output filename
outputFile := filepath.Join(outputFolder, fileName+".go")
// Write the file
if err := os.WriteFile(outputFile, formatted, 0644); err != nil {
return fmt.Errorf("error writing file: %w", err)
}
return nil
Conclusion
This is when we admire the final result and create a file like this:
box.json
{
"ui_type": "box",
"data": {
"title": "Box 1",
"description": "This is the first box",
"style": {
"backgroundColor": "red",
"color": "white",
"size": 2.5,
"borderRadius": 10,
"borderDash": [10, 10]
}
}
}
becomes something like this for our Server-Driven UI
box.go
// Code generated by frog; DO NOT EDIT.
package frog
type BoxStyle struct {
Size float64 `json:"size"`
BorderRadius int `json:"borderRadius"`
BorderDash []int `json:"borderDash"`
BackgroundColor string `json:"backgroundColor"`
Color string `json:"color"`
}
type Box struct {
Title string `json:"title"`
Description string `json:"description"`
Style BoxStyle `json:"style"`
}
If you want to see the complete code, just look at the LINK