simple_json

User Guide

Getting Started

Installation

  1. Set the environment variable:
    export SIMPLE_EIFFEL=/d/prod
  2. Add to your ECF file:
    <library name="simple_json" location="$SIMPLE_EIFFEL/simple_json/simple_json.ecf"/>

Your First JSON Parser

class
    MY_APP

feature
    json: SIMPLE_JSON
        once
            create Result
        end

    demo
        local
            value: SIMPLE_JSON_VALUE
        do
            -- Parse JSON text
            if attached json.parse ('{"name": "Alice", "age": 30}') as v then
                print (v.as_object.string_item ("name"))  -- "Alice"
                print (v.as_object.integer_item ("age"))   -- 30
            end
        end
end

Parsing JSON

Parse from String

local
    json: SIMPLE_JSON
    value: SIMPLE_JSON_VALUE
do
    create json

    -- Parse object
    if attached json.parse ('{"key": "value"}') as v then
        -- Use v
    end

    -- Parse array
    if attached json.parse ('[1, 2, 3]') as arr then
        -- Use arr
    end
end

Parse from File

if attached json.parse_file ("config.json") as config then
    -- Use config
else
    print (json.errors_as_string)
end

Validate Without Parsing

if json.is_valid_json (user_input) then
    -- Safe to parse
else
    print ("Invalid JSON: " + json.errors_as_string)
end

Error Handling

if attached json.parse (bad_json) as v then
    -- Success
else
    -- Detailed error information
    if json.has_errors then
        print ("Error count: " + json.error_count.out)

        -- First error only
        if attached json.first_error as err then
            print (err.message)
            print ("Line: " + err.line.out)
            print ("Column: " + err.column.out)
        end

        -- All errors with context
        print (json.detailed_errors)
    end
end

Building JSON

Build Objects

local
    json: SIMPLE_JSON
    obj: SIMPLE_JSON_OBJECT
do
    create json

    -- Fluent builder pattern
    obj := json.new_object
        .put_string ("name", "Alice")
        .put_integer ("age", 30)
        .put_boolean ("active", True)
        .put_null ("nickname")

    print (obj.to_json)
    -- {"name":"Alice","age":30,"active":true,"nickname":null}
end

Build Arrays

local
    arr: SIMPLE_JSON_ARRAY
do
    arr := json.new_array
        .add_string ("apple")
        .add_string ("banana")
        .add_integer (42)
        .add_boolean (True)

    print (arr.to_json)  -- ["apple","banana",42,true]
end

Nested Structures

local
    person: SIMPLE_JSON_OBJECT
    address: SIMPLE_JSON_OBJECT
    hobbies: SIMPLE_JSON_ARRAY
do
    -- Build address sub-object
    address := json.new_object
        .put_string ("street", "123 Main St")
        .put_string ("city", "Boston")

    -- Build hobbies array
    hobbies := json.new_array
        .add_string ("reading")
        .add_string ("coding")

    -- Build person with nested data
    person := json.new_object
        .put_string ("name", "Alice")
        .put_object ("address", address)
        .put_array ("hobbies", hobbies)

    print (person.to_json)
end

JSONPath Queries

JSONPath lets you query JSON using path expressions, similar to XPath for XML.

Basic Path Queries

local
    json: SIMPLE_JSON
    v: SIMPLE_JSON_VALUE
do
    if attached json.parse ('{"person": {"name": "Alice", "age": 30}}') as value then
        -- Query nested string
        if attached json.query_string (value, "$.person.name") as name then
            print (name)  -- "Alice"
        end

        -- Query nested integer
        print (json.query_integer (value, "$.person.age"))  -- 30
    end
end

Array Access

-- JSON: {"users": [{"name": "Alice"}, {"name": "Bob"}]}

-- Access specific array element (0-indexed)
json.query_string (v, "$.users[0].name")  -- "Alice"
json.query_string (v, "$.users[1].name")  -- "Bob"

Wildcard Queries

-- JSON: {"hobbies": ["reading", "coding", "gaming"]}

-- Get all array elements
local
    hobbies: ARRAYED_LIST [STRING_32]
do
    hobbies := json.query_strings (v, "$.hobbies[*]")
    -- ["reading", "coding", "gaming"]
end

-- JSON: {"people": [{"name": "Alice"}, {"name": "Bob"}]}

-- Get all names from array of objects
names := json.query_strings (v, "$.people[*].name")
-- ["Alice", "Bob"]

JSON Pointer (RFC 6901)

JSON Pointer is a simpler alternative to JSONPath, using forward slashes.

local
    pointer: SIMPLE_JSON_POINTER
    v: SIMPLE_JSON_VALUE
do
    -- Parse JSON
    if attached json.parse ('{"users": [{"name": "Alice"}]}') as root then
        create pointer

        -- Navigate with pointer
        if pointer.parse_path ("/users/0/name") then
            if attached pointer.value_at (root) as val then
                print (val.as_string_32)  -- "Alice"
            end
        end
    end
end

JSONPath vs JSON Pointer

JSONPath JSON Pointer Description
$.name /name Root property
$.users[0] /users/0 Array element
$.users[*].name N/A Wildcards (JSONPath only)

JSON Schema Validation

simple_json supports JSON Schema Draft 7 validation - the only Eiffel library with this capability.

Basic Schema Validation

local
    schema: SIMPLE_JSON_SCHEMA
    validator: SIMPLE_JSON_SCHEMA_VALIDATOR
    result: SIMPLE_JSON_SCHEMA_VALIDATION_RESULT
do
    -- Define schema
    create schema.make_from_string ('{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer", "minimum": 0}}}')

    -- Create validator
    create validator.make

    -- Validate data
    if attached json.parse ('{"name": "Alice", "age": 30}') as data then
        result := validator.validate (data, schema)

        if result.is_valid then
            print ("Valid!")
        else
            across result.errors as err loop
                print (err.message)
            end
        end
    end
end

Supported Schema Keywords

JSON Patch (RFC 6902)

JSON Patch lets you describe modifications to a JSON document.

Create and Apply Patches

local
    patch: SIMPLE_JSON_PATCH
    result: SIMPLE_JSON_PATCH_RESULT
do
    -- Create patch
    patch := json.create_patch
    patch.add_add ("/email", json.string_value ("alice@example.com"))
    patch.add_replace ("/age", json.integer_value (31))
    patch.add_remove ("/temporary")

    -- Apply to document
    if attached json.parse (original_json) as doc then
        result := patch.apply (doc)

        if result.is_success then
            print (result.result_document.to_json)
        else
            print ("Patch failed: " + result.error_message)
        end
    end
end

Patch Operations

Operation Method Description
add add_add (path, value) Add value at path
remove add_remove (path) Remove value at path
replace add_replace (path, value) Replace value at path
move add_move (from, path) Move value from one path to another
copy add_copy (from, path) Copy value from one path to another
test add_test (path, value) Test that path equals value

Parse Patch from JSON

local
    patch_json: STRING
    result: SIMPLE_JSON_PATCH_RESULT
do
    patch_json := '[{"op": "add", "path": "/email", "value": "alice@example.com"}, {"op": "replace", "path": "/age", "value": 31}]'

    result := json.apply_patch (document, patch_json)
end

JSON Merge Patch (RFC 7386)

A simpler alternative to JSON Patch - just provide the fields you want to change.

local
    merge: SIMPLE_JSON_MERGE_PATCH
    result: SIMPLE_JSON_MERGE_PATCH_RESULT
do
    -- Original: {"name": "Alice", "age": 30, "temp": true}
    -- Patch: {"age": 31, "email": "alice@ex.com", "temp": null}
    -- Result: {"name": "Alice", "age": 31, "email": "alice@ex.com"}

    create merge
    result := merge.apply (original, patch)

    if result.is_success then
        print (result.result_document.to_json)
    end
end

Merge Patch Rules

  • Properties in patch replace existing properties
  • New properties are added
  • null values remove properties
  • Properties not in patch are unchanged

Streaming Parser

For large files, use the streaming parser to process JSON incrementally.

local
    stream: SIMPLE_JSON_STREAM
    cursor: SIMPLE_JSON_STREAM_CURSOR
do
    create stream.make_from_file ("large_file.json")

    from
        cursor := stream.new_cursor
    until
        cursor.after
    loop
        if cursor.is_object_start then
            print ("Object started")
        elseif cursor.is_key then
            print ("Key: " + cursor.key_name)
        elseif cursor.is_string then
            print ("Value: " + cursor.string_value)
        end
        cursor.forth
    end
end

Entity Serialization

Implement SIMPLE_JSON_SERIALIZABLE for automatic JSON conversion.

class
    PERSON

inherit
    SIMPLE_JSON_SERIALIZABLE

feature -- Access

    name: STRING_32
    age: INTEGER
    email: detachable STRING_32

feature -- Serialization

    to_json_object: SIMPLE_JSON_OBJECT
        do
            Result := json.new_object
                .put_string ("name", name)
                .put_integer ("age", age)
            if attached email as e then
                Result.put_string ("email", e)
            end
        end

    from_json (a_value: SIMPLE_JSON_VALUE)
        do
            if a_value.is_object then
                if attached a_value.as_object.string_item ("name") as n then
                    name := n
                end
                age := a_value.as_object.integer_item ("age")
                email := a_value.as_object.string_item ("email")
            end
        end

end

Usage

local
    person: PERSON
do
    -- Serialize to JSON
    create person
    person.set_name ("Alice")
    person.set_age (30)
    print (person.to_json)

    -- Deserialize from JSON
    if attached json.parse (json_text) as v then
        create person
        person.from_json (v)
    end
end

Unicode Support

simple_json uses STRING_32 throughout for proper Unicode support.

local
    obj: SIMPLE_JSON_OBJECT
do
    -- Unicode strings work natively
    obj := json.new_object
        .put_string ("greeting", "Hello")
        .put_string ("emoji", "!")

    print (obj.to_json)
    -- Properly encoded UTF-8 output
end