simple_json

Architecture Guide

Architectural Overview

simple_json is designed as a high-level facade over the standard Eiffel JSON library (eJSON), adding modern features like JSON Schema validation, JSON Pointer, JSON Patch, and JSONPath queries while maintaining full compatibility with the underlying parser.

Design Philosophy

  • Facade Pattern: Simple API hiding complex internals
  • Builder Pattern: Fluent interface for constructing JSON
  • Wrapper Pattern: Type-safe wrappers around JSON values
  • Standards Compliance: RFC 6901, RFC 6902, RFC 7386, JSON Schema Draft 7

Class Hierarchy

SIMPLE_JSON_CONSTANTS (shared constants)
    |
    +-- SIMPLE_JSON (facade)
    |
    +-- SIMPLE_JSON_VALUE (value wrapper)
    |
    +-- SIMPLE_JSON_OBJECT (object builder)
    |
    +-- SIMPLE_JSON_ARRAY (array builder)
    |
    +-- SIMPLE_JSON_POINTER (RFC 6901)
    |
    +-- SIMPLE_JSON_SCHEMA_VALIDATOR
    |
    +-- SIMPLE_JSON_PATCH
    |       |
    |       +-- SIMPLE_JSON_PATCH_OPERATION (abstract)
    |               |
    |               +-- SIMPLE_JSON_PATCH_ADD
    |               +-- SIMPLE_JSON_PATCH_REMOVE
    |               +-- SIMPLE_JSON_PATCH_REPLACE
    |               +-- SIMPLE_JSON_PATCH_MOVE
    |               +-- SIMPLE_JSON_PATCH_COPY
    |               +-- SIMPLE_JSON_PATCH_TEST
    |
    +-- SIMPLE_JSON_MERGE_PATCH (RFC 7386)
    |
    +-- SIMPLE_JSON_STREAM (streaming parser)
            |
            +-- SIMPLE_JSON_STREAM_CURSOR
            +-- SIMPLE_JSON_STREAM_ELEMENT

Module Organization

Module Contents Purpose
src/core/ SIMPLE_JSON, SIMPLE_JSON_VALUE, SIMPLE_JSON_OBJECT, SIMPLE_JSON_ARRAY Core parsing and building
src/pointer/ SIMPLE_JSON_POINTER RFC 6901 navigation
src/schema/ SIMPLE_JSON_SCHEMA, SIMPLE_JSON_SCHEMA_VALIDATOR JSON Schema validation
src/patch/ SIMPLE_JSON_PATCH, patch operations RFC 6902 modifications
src/merge_patch/ SIMPLE_JSON_MERGE_PATCH RFC 7386 merge
src/streaming/ SIMPLE_JSON_STREAM Large file processing
src/utilities/ SIMPLE_JSON_PRETTY_PRINTER Formatting utilities
src/constants/ SIMPLE_JSON_CONSTANTS Shared constants

Relationship to eJSON

simple_json wraps the standard Eiffel JSON library (eJSON) rather than replacing it:

                    +-------------------+
                    |   SIMPLE_JSON     |  (Facade)
                    +-------------------+
                            |
            +---------------+---------------+
            |                               |
    +---------------+               +---------------+
    |SIMPLE_JSON_   |               |SIMPLE_JSON_   |
    |    VALUE      |               |    OBJECT     |
    +---------------+               +---------------+
            |                               |
            |  wraps                        |  wraps
            v                               v
    +---------------+               +---------------+
    |  JSON_VALUE   |               |  JSON_OBJECT  |  (eJSON)
    +---------------+               +---------------+

Why Wrap Instead of Replace?

What simple_json Adds

Facade Pattern

The SIMPLE_JSON class serves as the primary facade, providing a simplified interface for common operations:

class
    SIMPLE_JSON

feature -- Parsing
    parse (a_json_text: STRING_32): detachable SIMPLE_JSON_VALUE
    parse_file (a_file_path: STRING_32): detachable SIMPLE_JSON_VALUE
    is_valid_json (a_json_text: STRING_32): BOOLEAN

feature -- Building
    new_object: SIMPLE_JSON_OBJECT
    new_array: SIMPLE_JSON_ARRAY
    string_value (a_string: STRING_32): SIMPLE_JSON_VALUE
    -- ... more value creators

feature -- Queries
    query_string (a_value: SIMPLE_JSON_VALUE; a_path: STRING_32): detachable STRING_32
    -- ... more query methods

feature -- Patching
    create_patch: SIMPLE_JSON_PATCH
    apply_patch (...): SIMPLE_JSON_PATCH_RESULT

feature -- Errors
    has_errors: BOOLEAN
    last_errors: ARRAYED_LIST [SIMPLE_JSON_ERROR]
    -- ... more error methods
end

Benefits of Facade

  • Single entry point - one class to learn
  • Discoverable API - related features grouped together
  • Hides complexity - no need to know about eJSON internals
  • Consistent error handling across all operations

Builder Pattern

JSON construction uses a fluent builder pattern where methods return like Current for chaining:

class
    SIMPLE_JSON_OBJECT

feature -- Builder

    put_string (a_key, a_value: STRING_32): like Current
        do
            -- Add string to underlying JSON_OBJECT
            Result := Current
        ensure
            chained: Result = Current
            has_key: has (a_key)
        end

    put_integer (a_key: STRING_32; a_value: INTEGER_64): like Current
        do
            -- Add integer to underlying JSON_OBJECT
            Result := Current
        ensure
            chained: Result = Current
        end

end

Usage

-- Fluent construction
obj := json.new_object
    .put_string ("name", "Alice")
    .put_integer ("age", 30)
    .put_boolean ("active", True)
    .put_object ("address", json.new_object
        .put_string ("city", "Boston")
        .put_string ("state", "MA"))

Wrapper Pattern

SIMPLE_JSON_VALUE wraps JSON_VALUE to provide type-safe access with Unicode support:

class
    SIMPLE_JSON_VALUE

feature {NONE}
    json_value: JSON_VALUE  -- Wrapped eJSON value

feature -- Type checking
    is_string: BOOLEAN
        do Result := json_value.is_string end

    is_object: BOOLEAN
        do Result := json_value.is_object end

feature -- Access
    as_string_32: STRING_32
        require
            is_string: is_string
        do
            -- Convert from eJSON STRING_8 to STRING_32
        end

    as_object: SIMPLE_JSON_OBJECT
        require
            is_object: is_object
        do
            -- Wrap JSON_OBJECT in SIMPLE_JSON_OBJECT
        end

end

Key Design Decisions

Error Handling Architecture

The library uses a structured error model with position tracking:

class
    SIMPLE_JSON_ERROR

feature -- Access
    message: STRING_32
    position: INTEGER       -- Character offset in source
    line: INTEGER          -- Calculated line number
    column: INTEGER        -- Calculated column number

feature -- Output
    to_detailed_string: STRING_32
        -- Shows error with source context:
        -- Line 5, Column 12: Unexpected token
        --     {"name": "Alice", age: 30}
        --                      ^
end

Error Flow

parse() called
    |
    v
JSON_PARSER.parse_content
    |
    +-- Success --> Create SIMPLE_JSON_VALUE
    |
    +-- Failure --> capture_parser_errors()
                        |
                        v
                    Extract position from error string
                    Calculate line/column from position
                    Create SIMPLE_JSON_ERROR with context
                    Add to last_errors list

JSONPath Implementation

JSONPath queries are implemented as path parsing + recursive navigation:

-- Path: $.users[0].name
-- Segments: ["users", "[0]", "name"]

query_single_value (a_value: SIMPLE_JSON_VALUE; a_path: STRING_32)
    local
        l_segments: LIST [STRING_32]
        l_current: detachable SIMPLE_JSON_VALUE
    do
        l_segments := parse_json_path (a_path)  -- Split into segments
        l_current := a_value

        across l_segments as ic until l_current = Void loop
            l_current := navigate_segment (l_current, ic)  -- Navigate one level
        end

        Result := l_current
    end

Wildcard Expansion

-- Path: $.users[*].name
-- Expands [*] to iterate all array elements

query_multiple_values (a_value: SIMPLE_JSON_VALUE; a_path: STRING_32)
    do
        -- Maintains SET of current values
        -- At wildcard segments, expands set
        -- At regular segments, navigates each
    end

JSON Pointer (RFC 6901)

Simpler than JSONPath - just forward-slash separated segments:

-- Pointer: /users/0/name
-- Segments: ["users", "0", "name"]

parse_path (a_path: STRING_32): BOOLEAN
    do
        -- Split by "/"
        -- Unescape ~0 (tilde) and ~1 (slash)
        -- Store in segments list
    end

value_at (a_root: SIMPLE_JSON_VALUE): detachable SIMPLE_JSON_VALUE
    do
        -- Navigate each segment
        -- For objects: lookup by key
        -- For arrays: parse segment as integer index
    end

JSON Patch (RFC 6902)

Patch operations are implemented as a command pattern:

deferred class
    SIMPLE_JSON_PATCH_OPERATION

feature
    path: STRING_32
    is_valid: BOOLEAN
    apply (a_document: SIMPLE_JSON_VALUE): SIMPLE_JSON_PATCH_RESULT
        deferred
        end
end

class SIMPLE_JSON_PATCH_ADD
inherit SIMPLE_JSON_PATCH_OPERATION
feature
    value: SIMPLE_JSON_VALUE

    apply (a_document: SIMPLE_JSON_VALUE): SIMPLE_JSON_PATCH_RESULT
        do
            -- Navigate to parent of path
            -- Add value at final segment
        end
end

Patch Application

SIMPLE_JSON_PATCH.apply (document)
    |
    v
For each operation in operations:
    |
    +-- Parse path into JSON Pointer
    |
    +-- Navigate to target location
    |
    +-- Execute operation (add/remove/replace/move/copy/test)
    |
    +-- On failure: return error result immediately
    |
    +-- On success: continue to next operation
    |
    v
Return success with modified document

Schema Validation Architecture

JSON Schema validation follows Draft 7 specification:

SIMPLE_JSON_SCHEMA_VALIDATOR.validate (instance, schema)
    |
    +-- validate_type (instance, schema)
    |       |
    |       +-- Check "type" keyword matches instance type
    |
    +-- Type-specific validation:
            |
            +-- String: minLength, maxLength, pattern
            +-- Number: minimum, maximum
            +-- Object: properties, required, additionalProperties
            +-- Array: items, minItems, maxItems

Validation Result

class
    SIMPLE_JSON_SCHEMA_VALIDATION_RESULT

feature
    is_valid: BOOLEAN
    errors: ARRAY [SIMPLE_JSON_SCHEMA_VALIDATION_ERROR]
        -- Each error has:
        --   path: JSON Pointer to invalid location
        --   message: Human-readable description
        --   keyword: Schema keyword that failed
end

Streaming Parser Architecture

For large files, the streaming parser processes JSON token-by-token:

SIMPLE_JSON_STREAM
    |
    +-- Character buffer (file or string)
    |
    +-- Tokenizer (produces tokens)
    |
    +-- Cursor (iterates tokens)
            |
            +-- is_object_start, is_object_end
            +-- is_array_start, is_array_end
            +-- is_key, is_string, is_number
            +-- forth (advance to next token)

Memory Efficiency

Unlike DOM parsing which loads entire document into memory, streaming:

Unicode Handling

All string operations use STRING_32 for proper Unicode support:

-- Internal conversion
utf_converter: UTF_CONVERTER
    once
        create Result
    end

-- String input: STRING_32 -> UTF-8 -> eJSON parser
l_utf8 := utf_converter.utf_32_string_to_utf_8_string_8 (a_json_text)

-- String output: eJSON -> UTF-8 -> STRING_32
Result := utf_converter.utf_8_string_8_to_string_32 (l_json_string)

Design by Contract

The library uses comprehensive contracts throughout:

feature -- Parsing

    parse (a_json_text: STRING_32): detachable SIMPLE_JSON_VALUE
        require
            not_empty: not a_json_text.is_empty
        ensure
            errors_cleared_on_success: Result /= Void implies not has_errors

invariant
    -- Error list integrity
    last_errors_attached: last_errors /= Void

    -- Error state consistency
    has_errors_definition: has_errors = not last_errors.is_empty

    -- No void errors in list
    no_void_errors: across last_errors as ic all ic /= Void end

SCOOP Compatibility

The library is designed for SCOOP (Simple Concurrent Object-Oriented Programming):

-- Safe in SCOOP context
separate
    json_processor: SIMPLE_JSON
do
    -- Each processor has its own error state
    -- No shared mutable state
end