How Neo.Json Implements JSON Serialization for Blockchain Entities and Contract Parameters

Neo.Json uses a lightweight JToken hierarchy where blockchain entities like Transaction and ContractParameter implement ToJson() methods that assemble JObject instances, enabling deterministic, dependency-free JSON serialization for RPC responses and smart contract interactions.

The neo-project/neo repository provides a custom JSON handling layer in the Neo.Json namespace. This infrastructure enables JSON serialization for blockchain entities and contract parameters by allowing each domain type to convert itself into a token-based representation. The resulting JObject graphs can be written to strings via JToken.ToString() or transmitted directly over the wire.

Core JSON Infrastructure in Neo.Json

The serialization system rests on an abstract JToken hierarchy that mirrors the JSON data model. These classes handle parsing, property storage, and recursive writing without external dependencies.

JToken Base Class

Located in [src/Neo.Json/JToken.cs](https://github.com/neo-project/neo/blob/master-n3/src/Neo.Json/JToken.cs), the abstract JToken class provides the foundation for all JSON values. It exposes static methods for parsing raw JSON and instance methods for writing to a Utf8JsonWriter. The ToString() overload enables indented or compact string output, while ToByteArray() returns the UTF-8 encoded payload.

JObject and Property Storage

[src/Neo.Json/JObject.cs](https://github.com/neo-project/neo/blob/master-n3/src/Neo.Json/JObject.cs) represents JSON objects using an OrderedDictionary<string, JToken?>. This preserves insertion order during iteration. The class implements an indexer (obj["name"]) for property assignment and overrides Write to serialize each key-value pair to the underlying writer.

JArray and Primitive Types

Arrays are handled by [src/Neo.Json/JArray.cs](https://github.com/neo-project/neo/blob/master-n3/src/Neo.Json/JArray.cs), which stores elements in a List<JToken?>. Primitive JSON types reside in separate files:

Utility Helpers

[src/Neo.Json/Utility.cs](https://github.com/neo-project/neo/blob/master-n3/src/Neo.Json/Utility.cs) provides StrictUTF8 for consistent UTF-8 encoding and the ordered dictionary implementation used by JObject.

Serializing Contract Parameters with Circular Reference Protection

Smart contract arguments use the ContractParameter type, which implements a robust ToJson method to handle complex nested structures including arrays and maps.

The ToJson Implementation

In [src/Neo/SmartContract/ContractParameter.cs](https://github.com/neo-project/neo/blob/master-n3/src/Neo/SmartContract/ContractParameter.cs), the ToJson method constructs a JObject containing a type field and an optional value field. The implementation distinguishes between primitive types and collections:

public JObject ToJson()
{
    return ToJson(this, null);
}

private static JObject ToJson(ContractParameter parameter, HashSet<ContractParameter>? context)
{
    JObject json = new();
    json["type"] = parameter.Type;
    if (parameter.Value != null)
        switch (parameter.Type)
        {
            case ContractParameterType.Signature:
            case ContractParameterType.ByteArray:
                json["value"] = Convert.ToBase64String((byte[])parameter.Value);
                break;
            case ContractParameterType.Boolean:
                json["value"] = (bool)parameter.Value;
                break;
            case ContractParameterType.Integer:
            case ContractParameterType.Hash160:
            case ContractParameterType.Hash256:
            case ContractParameterType.PublicKey:
            case ContractParameterType.String:
                json["value"] = parameter.Value.ToString();
                break;
            case ContractParameterType.Array:
                // Detect circular references
                context ??= [];
                if (!context.Add(parameter)) throw new InvalidOperationException("Circular reference.");
                json["value"] = new JArray(((IList<ContractParameter>)parameter.Value)
                                        .Select(p => ToJson(p, context)));
                context.Remove(parameter);
                break;
            case ContractParameterType.Map:
                // Same guard as Array
                context ??= [];
                if (!context.Add(parameter)) throw new InvalidOperationException("Circular reference.");
                json["value"] = new JArray(((IList<KeyValuePair<ContractParameter, ContractParameter>>)parameter.Value)
                                        .Select(p => {
                                            var item = new JObject();
                                            item["key"]   = ToJson(p.Key, context);
                                            item["value"] = ToJson(p.Value, context);
                                            return item;
                                        }));
                context.Remove(parameter);
                break;
        }
    return json;
}

Handling Complex Types

The method encodes binary data as base-64 for ByteArray and Signature types. For Array and Map types, it recursively processes child parameters while using a HashSet<ContractParameter> to detect and prevent circular references. This ensures that deeply nested contract arguments serialize safely without infinite loops.

Serializing Blockchain Entities: The Transaction Example

Full blockchain entities follow the same pattern, aggregating multiple nested objects into a single JSON representation. The Transaction class demonstrates this approach for complex payloads.

Transaction.ToJson Implementation

In [src/Neo/Network/P2P/Payloads/Transaction.cs](https://github.com/neo-project/neo/blob/master-n3/src/Neo/Network/P2P/Payloads/Transaction.cs), the ToJson method constructs a comprehensive JSON object containing transaction metadata, signer information, and witness data:

public JObject ToJson(ProtocolSettings settings)
{
    JObject json = new();
    json["hash"]              = Hash.ToString();
    json["size"]              = Size;
    json["version"]           = Version;
    json["nonce"]             = Nonce;
    json["sender"]            = Sender.ToAddress(settings.AddressVersion);
    json["sysfee"]            = SystemFee.ToString();
    json["netfee"]            = NetworkFee.ToString();
    json["validuntilblock"]   = ValidUntilBlock;
    json["signers"]           = Signers.Select(p => p.ToJson()).ToArray();
    json["attributes"]        = Attributes.Select(p => p.ToJson()).ToArray();
    json["script"]            = Convert.ToBase64String(Script.Span);
    json["witnesses"]         = Witnesses.Select(p => p.ToJson()).ToArray();
    return json;
}

Aggregating Nested Objects

The method aggregates signers, attributes, and witnesses by calling their respective ToJson() methods and collecting the results into JArray instances. Binary data such as the invocation script is encoded as base-64 to ensure valid JSON output. This hierarchical approach allows entire blocks, transactions, and contract invocation parameters to serialize into complete, protocol-compliant JSON documents.

Practical Usage Examples

Example 1: Serializing a Contract Parameter

The following example demonstrates creating a nested array parameter and converting it to JSON:

using Neo.SmartContract;
using Neo.Json;

// Create a nested parameter: [ "hello", 42, true ]
var arrayParam = new ContractParameter(ContractParameterType.Array)
{
    Value = new List<ContractParameter>
    {
        new ContractParameter(ContractParameterType.String) { Value = "hello" },
        new ContractParameter(ContractParameterType.Integer) { Value = new System.Numerics.BigInteger(42) },
        new ContractParameter(ContractParameterType.Boolean) { Value = true }
    }
};

JObject json = arrayParam.ToJson();      // ↳ builds a JObject
string pretty = json.ToString(true);      // indented JSON string
Console.WriteLine(pretty);

Result (formatted):

{
  "type": "Array",
  "value": [
    { "type": "String", "value": "hello" },
    { "type": "Integer", "value": "42" },
    { "type": "Boolean", "value": true }
  ]
}

Example 2: Serializing a Transaction

To serialize a complete transaction with all nested components:

using Neo.Network.P2P.Payloads;
using Neo.Json;
using Neo.SmartContract;

// Assume `tx` is an already-deserialized Transaction instance
ProtocolSettings settings = ProtocolSettings.Default;
JObject txJson = tx.ToJson(settings);
string jsonString = txJson.ToString();   // compact representation
Console.WriteLine(jsonString);

The output contains the transaction hash, size, signer list, attributes, base-64 script, and witnesses.

Key Source Files and Their Roles

Summary

  • Type-centric architecture: Every blockchain entity and contract parameter implements ToJson() to assemble JObject graphs, ensuring consistent JSON serialization for blockchain entities and contract parameters across the Neo platform.
  • Lightweight infrastructure: The JToken hierarchy in src/Neo.Json provides parsing, property storage, and UTF-8 writing without external dependencies.
  • Circular reference safety: ContractParameter.ToJson uses a HashSet<ContractParameter> context to detect and prevent infinite recursion in nested arrays and maps.
  • Binary data handling: Byte arrays and transaction scripts are encoded as base-64 strings to maintain valid JSON output while preserving exact byte sequences.
  • Hierarchical aggregation: Complex types like Transaction aggregate nested JObject instances (signers, witnesses) into arrays, producing complete protocol-compliant JSON documents.

Frequently Asked Questions

How does Neo.Json prevent infinite recursion when serializing nested contract parameters?

The ContractParameter.ToJson method in src/Neo/SmartContract/ContractParameter.cs accepts a HashSet<ContractParameter> context parameter. When serializing Array or Map types, the method attempts to add the current parameter to the set. If the add operation returns false (indicating the object is already present), the method throws an InvalidOperationException to break the circular reference. After processing children, the parameter is removed from the set to allow reuse in different branches of the object graph.

What encoding does Neo.Json use for binary data in JSON output?

Binary data such as byte arrays, signatures, and transaction scripts are encoded using base-64 encoding. This occurs in ContractParameter.ToJson for ByteArray and Signature types via Convert.ToBase64String, and in Transaction.ToJson for the script field. Base-64 encoding ensures that arbitrary binary data remains valid JSON strings while preserving exact byte sequences for cryptographic verification and script execution.

Can Neo.Json serialize complex blockchain entities like full transactions?

Yes. The Transaction.ToJson method in src/Neo/Network/P2P/Payloads/Transaction.cs demonstrates serialization of complex entities by aggregating multiple nested objects. It invokes ToJson() on each Signer, TransactionAttribute, and Witness, collecting the results into JArray instances. The method combines these with primitive fields (hash, size, fees) and base-64 encoded binary data to produce a complete JSON representation suitable for RPC responses and block explorers.

How do I convert a JObject to a formatted JSON string in Neo?

Call the ToString(bool indented) method on any JToken instance. Pass true for pretty-printed output with line breaks and indentation, or false (or omit the parameter) for compact output. Internally, this method writes the token to a Utf8JsonWriter and returns the UTF-8 string via StrictUTF8.GetString, as implemented in src/Neo.Json/JToken.cs.

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 →