How GeoIP Initializes Configuration from config.json or Byte Slices: A Deep Dive into the Instance Loader

GeoIP loads configuration through the Instance type in lib/instance.go, using InitConfig for file or URL paths and InitConfigFromBytes for raw JSON, with both methods ultimately parsing content via hujson.Standardize and custom UnmarshalJSON implementations that dynamically instantiate input and output converters from registered plugin creators.

The loyalsoldier/geoip project provides a flexible pipeline for converting GeoIP databases between various formats. Understanding how it handles initializing configuration from config.json files or byte slices requires examining the Instance implementation and its plugin registration architecture. This article explores the complete flow from file reading to dynamic converter instantiation.

The Instance Entry Points: InitConfig and InitConfigFromBytes

The core configuration loading logic resides in lib/instance.go, where the Instance type provides two public methods for initialization. According to the loyalsoldier/geoip source code, these methods form the primary API for configuration ingestion.

Loading from Local Files and Remote URLs

The InitConfig(configFile string) error method (lines 36‑50) handles both local filesystem paths and remote HTTP(S) endpoints. The implementation detects URLs by checking for http:// or https:// prefixes. For local files, it uses os.ReadFile to read the content into memory. For remote resources, it delegates to GetRemoteURLContent to fetch the configuration over the network. Regardless of the source, the method ultimately converts the content to a byte slice and delegates to InitConfigFromBytes.

Processing Raw JSON with hujson

The InitConfigFromBytes(content []byte) error method (lines 52‑71) serves as the central parsing engine. First, it invokes hujson.Standardize to strip JavaScript-style comments and trailing commas, transforming human-friendly JSON into strictly valid JSON. Then it unmarshals the cleaned content into the internal config struct using json.Unmarshal. This two-step process allows users to write configuration files with comments for documentation while maintaining standard JSON compatibility.

Configuration Structure and Dynamic Unmarshaling

The config type defined in lib/config.go (lines 55‑58) serves as the root data structure:

type config struct {
    Input  []*inputConvConfig  `json:"input"`
    Output []*outputConvConfig `json:"output"`
}

Each slice element implements a custom UnmarshalJSON method that enables dynamic plugin instantiation during the JSON parsing phase.

Input Converter Initialization

The inputConvConfig.UnmarshalJSON method (lines 66‑91) handles the input array elements. During unmarshaling, it extracts the type, action, and args fields from the JSON object. It validates that the specified action exists in the global ActionsRegistry. Then it invokes createInputConfig, a factory function registered via RegisterInputConfigCreator, to obtain an InputConverter implementation. This approach decouples the configuration format from the concrete converter implementations.

Output Converter Initialization

Similarly, the outputConvConfig.UnmarshalJSON method (lines 99‑127) processes the output array. This implementation defaults the action field to ActionOutput when the field is omitted in the JSON. It then calls createOutputConfig, registered via RegisterOutputConfigCreator, to construct the appropriate OutputConverter. Both unmarshaling methods perform case‑insensitive lookups in their respective registration caches.

Plugin Registration System

The dynamic instantiation relies on two registration maps: inputConfigCreatorCache and outputConfigCreatorCache. These map[string]func tables are populated by each plugin's init() function through the RegisterInputConfigCreator and RegisterOutputConfigCreator helpers (lines 19‑44). When the configuration unmarshals, the system looks up the creator function by the type field (case‑insensitive) and invokes it to construct the concrete converter instances. The resulting converters are stored in the Instance's internal input and output slices, ready for the conversion pipeline.

Complete Configuration Loading Example

The following example demonstrates both file‑based and byte‑slice configuration initialization:

package main

import (
    "log"
    "github.com/Loyalsoldier/geoip/lib"
)

func main() {
    // 1️⃣ Load configuration from a file on disk
    inst, err := lib.NewInstance()
    if err != nil {
        log.Fatal(err)
    }
    if err := inst.InitConfig("config.json"); err != nil {
        log.Fatal(err)
    }
    // Run the conversion pipeline
    if err := inst.Run(); err != nil {
        log.Fatal(err)
    }

    // 2️⃣ Load configuration from an in‑memory JSON blob (e.g. embedded or fetched elsewhere)
    jsonBlob := []byte(`{
        // comments are allowed
        "input": [{ "type": "maxmind_country_mmdb", "action": "input", "args": { "path": "GeoIP2-Country.mmdb" } }],
        "output": [{ "type": "plain_text", "action": "output", "args": { "file": "countries.txt" } }]
    }`)
    inst2, _ := lib.NewInstance()
    if err := inst2.InitConfigFromBytes(jsonBlob); err != nil {
        log.Fatal(err)
    }
    if err := inst2.Run(); err != nil {
        log.Fatal(err)
    }
}

Summary

  • Dual Entry Points: The Instance type provides InitConfig for file/URL paths and InitConfigFromBytes for raw byte slices, with both converging on the same parsing pipeline.
  • Comment Support: The hujson.Standardize function preprocesses JSON to remove comments and trailing commas, allowing human-readable configuration files.
  • Dynamic Plugin Instantiation: Custom UnmarshalJSON methods on inputConvConfig and outputConvConfig lookup and invoke registered creator functions to build concrete converters at runtime.
  • Registration Architecture: Plugins register themselves via RegisterInputConfigCreator and RegisterOutputConfigCreator, storing factory functions in global cache maps for case‑insensitive lookup during configuration loading.

Frequently Asked Questions

Does GeoIP support JSON with comments in config.json?

Yes. As implemented in lib/instance.go, the InitConfigFromBytes method uses hujson.Standardize to strip both single-line and multi-line comments, as well as trailing commas, before passing the content to json.Unmarshal. This allows you to document your configuration inline without breaking the parser.

How does GeoIP handle remote configuration files?

The InitConfig method detects remote URLs by checking for http:// or https:// prefixes. When detected, it fetches the content using GetRemoteURLContent rather than os.ReadFile, enabling centralized configuration management via HTTP endpoints before processing the bytes through the standard pipeline.

What happens if an action type is not registered?

During unmarshaling in lib/config.go, the inputConvConfig.UnmarshalJSON method validates that the specified action exists in the ActionsRegistry. If the action is not found, the unmarshaling process returns an error, preventing the initialization of unconfigured or mistyped converter pipelines.

Can configuration be loaded from memory without a file?

Yes. The InitConfigFromBytes method accepts a []byte slice directly, bypassing filesystem operations entirely. This enables scenarios such as embedded configuration assets, dynamically generated JSON, or configurations fetched through custom transport mechanisms not handled by the built-in URL fetcher.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →