simple_json

Cookbook

Cookbook Recipes

Real-world examples and patterns for common JSON tasks.

Recipe 1: Configuration File Reader

Load and access application configuration from JSON.

class
    APP_CONFIG

create
    make_from_file

feature {NONE} -- Initialization

    make_from_file (a_path: STRING)
        local
            json: SIMPLE_JSON
        do
            create json
            if attached json.parse_file (a_path) as v then
                load_from_json (v)
            else
                -- Use defaults or raise error
                set_defaults
                if json.has_errors then
                    io.put_string ("Config error: " + json.errors_as_string)
                end
            end
        end

feature -- Access

    database_host: STRING
    database_port: INTEGER
    debug_mode: BOOLEAN
    allowed_origins: ARRAYED_LIST [STRING]

feature {NONE} -- Implementation

    load_from_json (v: SIMPLE_JSON_VALUE)
        local
            json: SIMPLE_JSON
        do
            create json
            create allowed_origins.make (0)

            -- Use JSONPath for nested access
            if attached json.query_string (v, "$.database.host") as h then
                database_host := h
            else
                database_host := "localhost"
            end

            database_port := json.query_integer (v, "$.database.port").to_integer
            if database_port = 0 then
                database_port := 5432
            end

            -- Boolean with default
            if v.is_object then
                debug_mode := v.as_object.boolean_item ("debug")
            end

            -- Array of strings
            across json.query_strings (v, "$.cors.origins[*]") as origin loop
                allowed_origins.force (origin)
            end
        end

    set_defaults
        do
            database_host := "localhost"
            database_port := 5432
            debug_mode := False
            create allowed_origins.make (0)
        end

end

Example config.json

{
    "database": {
        "host": "db.example.com",
        "port": 5432
    },
    "debug": true,
    "cors": {
        "origins": ["http://localhost:3000", "https://app.example.com"]
    }
}

Recipe 2: REST API Response Builder

Build consistent JSON responses for web APIs.

class
    API_RESPONSE_BUILDER

feature -- Response Building

    success (a_data: SIMPLE_JSON_VALUE): STRING
            -- Build success response
        do
            Result := json.new_object
                .put_boolean ("success", True)
                .put_value ("data", a_data)
                .put_null ("error")
                .to_json
        end

    success_with_meta (a_data: SIMPLE_JSON_VALUE; a_page, a_total: INTEGER): STRING
            -- Build paginated response
        do
            Result := json.new_object
                .put_boolean ("success", True)
                .put_value ("data", a_data)
                .put_object ("meta", json.new_object
                    .put_integer ("page", a_page)
                    .put_integer ("total", a_total)
                    .put_integer ("per_page", 20))
                .put_null ("error")
                .to_json
        end

    error (a_code: INTEGER; a_message: STRING): STRING
            -- Build error response
        do
            Result := json.new_object
                .put_boolean ("success", False)
                .put_null ("data")
                .put_object ("error", json.new_object
                    .put_integer ("code", a_code)
                    .put_string ("message", a_message))
                .to_json
        end

    validation_error (a_errors: ARRAYED_LIST [TUPLE [field, message: STRING]]): STRING
            -- Build validation error response
        local
            errors_array: SIMPLE_JSON_ARRAY
        do
            errors_array := json.new_array
            across a_errors as e loop
                errors_array.add_object (json.new_object
                    .put_string ("field", e.field)
                    .put_string ("message", e.message))
            end

            Result := json.new_object
                .put_boolean ("success", False)
                .put_null ("data")
                .put_object ("error", json.new_object
                    .put_integer ("code", 422)
                    .put_string ("message", "Validation failed")
                    .put_array ("details", errors_array))
                .to_json
        end

feature {NONE} -- Implementation

    json: SIMPLE_JSON
        once
            create Result
        end

end

Usage

local
    response: API_RESPONSE_BUILDER
    users: SIMPLE_JSON_ARRAY
do
    create response

    -- Success response
    users := json.new_array
        .add_object (json.new_object.put_string ("name", "Alice"))
        .add_object (json.new_object.put_string ("name", "Bob"))

    print (response.success_with_meta (users.to_value, 1, 42))
    -- {"success":true,"data":[...],"meta":{"page":1,"total":42}}

    -- Error response
    print (response.error (404, "User not found"))
    -- {"success":false,"data":null,"error":{"code":404,"message":"User not found"}}
end

Recipe 3: Request Validation with Schema

Validate incoming JSON requests against a schema.

class
    USER_VALIDATOR

feature -- Validation

    validate_create_user (a_json: STRING): TUPLE [valid: BOOLEAN; errors: STRING; data: detachable SIMPLE_JSON_VALUE]
            -- Validate user creation request
        local
            json: SIMPLE_JSON
            validator: SIMPLE_JSON_SCHEMA_VALIDATOR
            result: SIMPLE_JSON_SCHEMA_VALIDATION_RESULT
            error_text: STRING
        do
            create json
            create validator.make

            -- First, parse the JSON
            if attached json.parse (a_json) as v then
                -- Then validate against schema
                result := validator.validate (v, user_schema)

                if result.is_valid then
                    Result := [True, "", v]
                else
                    -- Collect error messages
                    create error_text.make_empty
                    across result.errors as e loop
                        if not error_text.is_empty then
                            error_text.append ("; ")
                        end
                        error_text.append (e.message)
                    end
                    Result := [False, error_text, Void]
                end
            else
                Result := [False, "Invalid JSON: " + json.errors_as_string, Void]
            end
        end

feature {NONE} -- Schemas

    user_schema: SIMPLE_JSON_SCHEMA
            -- Schema for user creation
        once
            create Result.make_from_string ("[
                {
                    "type": "object",
                    "required": ["email", "name"],
                    "properties": {
                        "email": {
                            "type": "string",
                            "pattern": "^[^@]+@[^@]+\\.[^@]+$"
                        },
                        "name": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                        },
                        "age": {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 150
                        }
                    }
                }
            ]")
        end

end

Usage in Web Handler

local
    validator: USER_VALIDATOR
    validation: TUPLE [valid: BOOLEAN; errors: STRING; data: detachable SIMPLE_JSON_VALUE]
do
    create validator
    validation := validator.validate_create_user (request_body)

    if validation.valid and attached validation.data as data then
        -- Process valid request
        create_user (data)
    else
        -- Return validation errors
        send_error (422, validation.errors)
    end
end

Recipe 4: Document Diff and Patch

Track and apply changes to JSON documents.

class
    DOCUMENT_TRACKER

feature -- Tracking

    track_changes (a_original, a_modified: SIMPLE_JSON_VALUE): SIMPLE_JSON_PATCH
            -- Generate patch representing changes from original to modified
        local
            patch: SIMPLE_JSON_PATCH
        do
            patch := json.create_patch

            if a_original.is_object and a_modified.is_object then
                diff_objects (a_original.as_object, a_modified.as_object, "", patch)
            end

            Result := patch
        end

    apply_and_log (a_document: SIMPLE_JSON_VALUE; a_patch: SIMPLE_JSON_PATCH): TUPLE [doc: SIMPLE_JSON_VALUE; log: STRING]
            -- Apply patch and return result with change log
        local
            result: SIMPLE_JSON_PATCH_RESULT
            log: STRING
        do
            result := a_patch.apply (a_document)

            create log.make_empty
            log.append ("Applied " + a_patch.operations.count.out + " operations%N")

            if result.is_success then
                log.append ("Status: Success")
                Result := [result.result_document, log]
            else
                log.append ("Status: Failed - " + result.error_message)
                Result := [a_document, log]  -- Return original on failure
            end
        end

feature {NONE} -- Implementation

    diff_objects (a_orig, a_mod: SIMPLE_JSON_OBJECT; a_path: STRING; a_patch: SIMPLE_JSON_PATCH)
            -- Recursively diff objects
        local
            key, path: STRING
        do
            -- Check for removed keys
            across a_orig.keys as k loop
                key := k
                path := a_path + "/" + key
                if not a_mod.has (key) then
                    a_patch.add_remove (path)
                end
            end

            -- Check for added/changed keys
            across a_mod.keys as k loop
                key := k
                path := a_path + "/" + key
                if not a_orig.has (key) then
                    -- New key
                    if attached a_mod.item (key) as v then
                        a_patch.add_add (path, v)
                    end
                elseif attached a_orig.item (key) as ov and attached a_mod.item (key) as mv then
                    -- Check if changed
                    if not values_equal (ov, mv) then
                        a_patch.add_replace (path, mv)
                    end
                end
            end
        end

    values_equal (a, b: SIMPLE_JSON_VALUE): BOOLEAN
        do
            Result := a.to_json.same_string (b.to_json)
        end

    json: SIMPLE_JSON
        once
            create Result
        end

end

Recipe 5: Entity Serialization

Serialize domain objects to/from JSON.

class
    ORDER

inherit
    SIMPLE_JSON_SERIALIZABLE

create
    make, make_from_json

feature {NONE} -- Initialization

    make (a_id: INTEGER; a_customer: STRING)
        do
            id := a_id
            customer := a_customer
            create items.make (0)
            status := "pending"
            created_at := create {DATE_TIME}.make_now
        end

    make_from_json (a_value: SIMPLE_JSON_VALUE)
        do
            from_json (a_value)
        end

feature -- Access

    id: INTEGER
    customer: STRING
    items: ARRAYED_LIST [ORDER_ITEM]
    status: STRING
    created_at: DATE_TIME

feature -- Serialization

    to_json_object: SIMPLE_JSON_OBJECT
        local
            items_array: SIMPLE_JSON_ARRAY
        do
            items_array := json.new_array
            across items as item loop
                items_array.add_object (item.to_json_object)
            end

            Result := json.new_object
                .put_integer ("id", id)
                .put_string ("customer", customer)
                .put_array ("items", items_array)
                .put_string ("status", status)
                .put_string ("created_at", created_at.formatted_out ("yyyy-mm-dd hh:mi:ss"))
        end

    from_json (a_value: SIMPLE_JSON_VALUE)
        local
            item: ORDER_ITEM
        do
            if a_value.is_object then
                id := a_value.as_object.integer_item ("id").to_integer

                if attached a_value.as_object.string_item ("customer") as c then
                    customer := c
                else
                    customer := ""
                end

                if attached a_value.as_object.string_item ("status") as s then
                    status := s
                else
                    status := "pending"
                end

                -- Parse items array
                create items.make (0)
                if attached a_value.as_object.array_item ("items") as arr then
                    across 1 |..| arr.count as i loop
                        create item.make_from_json (arr.item (i))
                        items.force (item)
                    end
                end
            end
        end

feature {NONE}

    json: SIMPLE_JSON
        once
            create Result
        end

end

Recipe 6: Processing Large JSON Files

Stream large JSON files without loading entirely into memory.

class
    LOG_ANALYZER

feature -- Analysis

    count_errors_in_log (a_file: STRING): INTEGER
            -- Count error entries in large JSON log file
            -- Format: [{"level": "error", ...}, {"level": "info", ...}, ...]
        local
            stream: SIMPLE_JSON_STREAM
            cursor: SIMPLE_JSON_STREAM_CURSOR
            in_object: BOOLEAN
            current_level: detachable STRING
        do
            create stream.make_from_file (a_file)
            cursor := stream.new_cursor

            from
            until
                cursor.after
            loop
                if cursor.is_object_start then
                    in_object := True
                    current_level := Void
                elseif cursor.is_object_end then
                    if attached current_level as lv and then lv.same_string ("error") then
                        Result := Result + 1
                    end
                    in_object := False
                elseif in_object and cursor.is_key then
                    if cursor.key_name.same_string ("level") then
                        cursor.forth
                        if cursor.is_string then
                            current_level := cursor.string_value
                        end
                    end
                end
                cursor.forth
            end
        end

    extract_timestamps (a_file: STRING; a_output: ARRAYED_LIST [STRING])
            -- Extract all timestamp fields from large file
        local
            stream: SIMPLE_JSON_STREAM
            cursor: SIMPLE_JSON_STREAM_CURSOR
            looking_for_value: BOOLEAN
        do
            create stream.make_from_file (a_file)
            cursor := stream.new_cursor

            from
            until
                cursor.after
            loop
                if cursor.is_key and then cursor.key_name.same_string ("timestamp") then
                    looking_for_value := True
                elseif looking_for_value and cursor.is_string then
                    a_output.force (cursor.string_value)
                    looking_for_value := False
                end
                cursor.forth
            end
        end

end

Recipe 7: Merging Configuration Files

Merge default config with user overrides using JSON Merge Patch.

class
    CONFIG_MERGER

feature -- Merging

    merge_configs (a_default_file, a_user_file: STRING): SIMPLE_JSON_VALUE
            -- Merge user config over default config
        local
            json: SIMPLE_JSON
            merge: SIMPLE_JSON_MERGE_PATCH
            result: SIMPLE_JSON_MERGE_PATCH_RESULT
        do
            create json
            create merge

            if attached json.parse_file (a_default_file) as default_config then
                if attached json.parse_file (a_user_file) as user_config then
                    -- Merge user settings over defaults
                    result := merge.apply (default_config, user_config)
                    if result.is_success then
                        Result := result.result_document
                    else
                        Result := default_config
                    end
                else
                    -- No user config, use defaults
                    Result := default_config
                end
            else
                -- Return empty object on failure
                Result := json.new_object.to_value
            end
        end

end

Example

-- default.json
{
    "theme": "light",
    "font_size": 14,
    "auto_save": true,
    "plugins": {
        "spell_check": true,
        "syntax_highlight": true
    }
}

-- user.json (overrides)
{
    "theme": "dark",
    "font_size": 16,
    "plugins": {
        "spell_check": false
    }
}

-- Result after merge
{
    "theme": "dark",           -- from user
    "font_size": 16,           -- from user
    "auto_save": true,         -- from default
    "plugins": {
        "spell_check": false,  -- from user
        "syntax_highlight": true  -- from default
    }
}

Recipe 8: JSON-to-JSON Transformation

Transform JSON structure for API version compatibility.

class
    API_TRANSFORMER

feature -- Transformation

    v1_to_v2 (a_v1: SIMPLE_JSON_VALUE): SIMPLE_JSON_VALUE
            -- Transform V1 API format to V2
            -- V1: {"first_name": "...", "last_name": "..."}
            -- V2: {"name": {"first": "...", "last": "..."}}
        local
            first, last: STRING
        do
            if a_v1.is_object then
                if attached a_v1.as_object.string_item ("first_name") as f then
                    first := f
                else
                    first := ""
                end

                if attached a_v1.as_object.string_item ("last_name") as l then
                    last := l
                else
                    last := ""
                end

                -- Build V2 structure
                Result := json.new_object
                    .put_object ("name", json.new_object
                        .put_string ("first", first)
                        .put_string ("last", last))
                    .to_value

                -- Copy other fields
                across a_v1.as_object.keys as k loop
                    if not k.same_string ("first_name") and not k.same_string ("last_name") then
                        if attached a_v1.as_object.item (k) as v then
                            Result.as_object.put_value (k, v)
                        end
                    end
                end
            else
                Result := a_v1
            end
        end

feature {NONE}

    json: SIMPLE_JSON
        once
            create Result
        end

end

Recipe 9: Comprehensive Error Handling

Handle all JSON error cases gracefully.

class
    SAFE_JSON_PROCESSOR

feature -- Processing

    process_safely (a_json: STRING): TUPLE [success: BOOLEAN; data: detachable SIMPLE_JSON_VALUE; error: STRING]
            -- Process JSON with comprehensive error handling
        local
            json: SIMPLE_JSON
            error_detail: STRING
        do
            create json

            -- Check for empty input
            if a_json.is_empty then
                Result := [False, Void, "Empty JSON input"]
            elseif attached json.parse (a_json) as v then
                -- Success
                Result := [True, v, ""]
            else
                -- Parse failed - build detailed error
                create error_detail.make_empty

                if json.has_errors then
                    if attached json.first_error as err then
                        error_detail.append ("Parse error at line ")
                        error_detail.append (err.line.out)
                        error_detail.append (", column ")
                        error_detail.append (err.column.out)
                        error_detail.append (": ")
                        error_detail.append (err.message)

                        -- Add context if available
                        if not err.to_detailed_string.is_empty then
                            error_detail.append ("%N")
                            error_detail.append (err.to_detailed_string)
                        end
                    else
                        error_detail.append ("Unknown parse error")
                    end
                else
                    error_detail.append ("Parse returned null without error")
                end

                Result := [False, Void, error_detail]
            end
        rescue
            -- Catch any unexpected exceptions
            Result := [False, Void, "Unexpected error processing JSON"]
        end

    safe_query (a_value: SIMPLE_JSON_VALUE; a_path: STRING; a_default: STRING): STRING
            -- Query with default value on failure
        local
            json: SIMPLE_JSON
        do
            create json
            if attached json.query_string (a_value, a_path) as s then
                Result := s
            else
                Result := a_default
            end
        end

end

Recipe 10: Pretty Printing for Debugging

Format JSON for human readability.

class
    JSON_DEBUGGER

feature -- Debugging

    debug_print (a_value: SIMPLE_JSON_VALUE)
            -- Print JSON with type annotations
        do
            io.put_string ("=== JSON Debug ===%N")
            io.put_string ("Type: " + type_name (a_value) + "%N")
            io.put_string ("Pretty:%N")
            io.put_string (a_value.to_pretty_json)
            io.put_string ("%N================%N")
        end

    compare_json (a_label1: STRING; a_json1: SIMPLE_JSON_VALUE; a_label2: STRING; a_json2: SIMPLE_JSON_VALUE)
            -- Side-by-side comparison
        do
            io.put_string ("=== " + a_label1 + " ===%N")
            io.put_string (a_json1.to_pretty_json)
            io.put_string ("%N%N=== " + a_label2 + " ===%N")
            io.put_string (a_json2.to_pretty_json)
            io.put_string ("%N")
        end

feature {NONE}

    type_name (a_value: SIMPLE_JSON_VALUE): STRING
        do
            if a_value.is_object then
                Result := "object (" + a_value.as_object.count.out + " keys)"
            elseif a_value.is_array then
                Result := "array (" + a_value.as_array.count.out + " items)"
            elseif a_value.is_string then
                Result := "string"
            elseif a_value.is_integer then
                Result := "integer"
            elseif a_value.is_number then
                Result := "number"
            elseif a_value.is_boolean then
                Result := "boolean"
            elseif a_value.is_null then
                Result := "null"
            else
                Result := "unknown"
            end
        end

end