Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

sm-json

Build StatusLatest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library#include<json>// this is where our encoding results will gocharoutput[1024];

Creating & Encoding

Arrays

JSON_Arrayarr=newJSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(newJSON_Array());
arr.PushObject(newJSON_Object());
arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=newJSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", newJSON_Array());
obj.SetObject("object", newJSON_Object());
obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Arraychild_arr=newJSON_Array();
child_arr.PushInt(1);
JSON_Objectchild_obj=newJSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);
JSON_Objectparent_obj=newJSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
"pretty_printing": true,
"first_depth": {
"im_indented": null,
"second_depth": [
1
]
}
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");
parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
charstrval[32];
arr.GetString(0, strval, sizeof(strval));
intintval=arr.GetInt(1);
floatfloatval=arr.GetFloat(2);
boolboolval=arr.GetBool(3);
Handlenullval=arr.GetObject(4);
JSON_Arrayarrval=view_as<JSON_Array>(arr.GetObject(5));
JSON_Objectobjval=arr.GetObject(6);
json_cleanup_and_delete(arr);

Objects

JSON_Objectobj=json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
charstrval[32];
obj.GetString("strkey", strval, sizeof(strval));
intintval=obj.GetInt("intkey");
floatfloatval=obj.GetFloat("floatkey");
boolboolval=obj.GetBool("boolkey");
Handlenullval=obj.GetObject("nullkey");
JSON_Arrayarrval=view_as<JSON_Array>(obj.GetObject("array"));
JSON_Objectobjval=obj.GetObject("object");
json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

intlength=arr.Length;
for (inti=0; i<length; i+=1) {
JSONCellTypetype=arr.GetType(i);
// do whatever you want with the index and type information
}

Objects

intlength=obj.Length;
intkey_length=0;
for (inti=0; i<length; i+=1) {
key_length=obj.GetKeySize(i);
char[] key=newchar[key_length];
obj.GetKey(i, key, key_length);
JSONCellTypetype=obj.GetType(key);
// do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr=null;
// orjson_cleanup_and_delete(arr);
obj.Cleanup();
obj=null;
// orjson_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Arrayshared=newJSON_Array();
JSON_Objectobj1=newJSON_Object();
obj1.SetObject("shared", shared);
JSON_Objectobj2=newJSON_Object();
obj2.SetObject("shared", shared);
// this will clean up the nested "shared" arrayjson_cleanup_and_delete(obj1);
// this will throw an Invalid Handle exception because "shared" no longer existsjson_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);
obj2.Remove("shared");
json_cleanup_and_delete(obj2);
json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmapPlayer<JSON_Object
{
publicboolSetAlias(constchar[] value)
{
returnthis.SetString("alias", value);
}
publicboolGetAlias(char[] buffer, intmax_size)
{
returnthis.GetString("alias", buffer, max_size);
}
propertyintScore
{
publicget()
{
returnthis.GetInt("score");
}
publicset(intvalue)
{
this.SetInt("score", value);
}
}
propertyfloatHeight
{
publicget()
{
returnthis.GetFloat("height");
}
publicset(floatvalue)
{
this.SetFloat("height", value);
}
}
propertyboolAlive
{
publicget()
{
returnthis.GetBool("alive");
}
publicset(boolvalue)
{
this.SetBool("alive", value);
}
}
propertyHandleHandle
{
publicget()
{
returnview_as<Handle>(this.GetObject("handle"));
}
publicset(Handlevalue)
{
this.SetObject("handle", value);
}
}
propertyJSON_ObjectObject
{
publicget()
{
returnthis.GetObject("object");
}
publicset(JSON_Objectvalue)
{
this.SetObject("object", value);
}
}
propertyJSON_ArrayArray
{
publicget()
{
returnview_as<JSON_Array>(this.GetObject("array"));
}
publicset(JSON_Arrayvalue)
{
this.SetObject("array", value);
}
}
publicPlayer()
{
Playerself=view_as<Player>(newJSON_Object());
self.SetAlias("clug");
self.Score=9001;
self.Height=1.8;
self.Alive= true;
self.Handle=null;
self.Object=newJSON_Object();
self.Array=newJSON_Array();
returnself;
}
publicvoidIncrementScore()
{
this.Score+=1;
}
}
Playerplayer=newPlayer();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmapWeapon<JSON_Object
{
propertyPlayerOwner
{
publicget()
{
returnview_as<Player>(this.GetObject("owner"));
}
publicset(Playervalue)
{
this.SetObject("owner", value);
}
}
propertyintId
{
publicget()
{
returnthis.GetInt("id");
}
publicset(intvalue)
{
this.SetInt("id", value);
}
}
publicWeapon()
{
Weaponself=view_as<Weapon>(newJSON_Object());
self.Id=1;
self.Owner=newPlayer();
returnself;
}
}
Weaponweapon=newWeapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Arrayarr=newJSON_Array();
JSON_Objectobj=newJSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
intlen=arr.GetSize(0);
char[] val=newchar[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Arrayints=newJSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1ints.PushInt(1); // returns 0json_cleanup_and_delete(ints);
JSON_Arrayvalues=newJSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain intsvalues.Remove(0);
values.EnforceType(JSON_Type_Int); // returns truejson_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

intints[] = {1, 2, 3};
JSON_Arrayarr=newJSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));
arr.Encode(output, sizeof(output)); // output now contains [1,2,3]json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

charstrings[][] = {"hello", "world"};
JSON_Arrayarr=newJSON_Array();
arr.ImportStrings(strings, sizeof(strings));
arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[1,2,3]"));
intsize=arr.Length;
int[] values=newint[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Arrayarr=view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
intsize=arr.Length;
intstr_length=arr.MaxStringLength;
char[][] values=newchar[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Objectobj1=newJSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);
JSON_Objectobj2=newJSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)
obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged// alternatively, without replacementobj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);
JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);
target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(newJSON_Array());
// arr is now equivocally [1,2,3,[]]JSON_Arraycopied=arr.ShallowCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", newJSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}JSON_Objectcopied=obj.ShallowCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Arraycopied=arr.DeepCopy();
JSON_Arraynested=view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Objectcopied=obj.DeepCopy();
JSON_Objectnested=copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Objectobj=json_decode(SOME_UNKNOWN_JSON);
JSON_Arrayarr=view_as<JSON_Array>(obj);
if (obj.IsArray) {
arr.PushString("ok");
} else {
obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// orjson_encode(obj/arr, output, sizeof(output) /*, options */);
obj/arr.ShallowCopy();
// orjson_copy_shallow(obj/arr);
obj/arr.DeepCopy();
// orjson_copy_deep(obj/arr);
obj/arr.Cleanup();
// orjson_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

About

A pure SourcePawn JSON encoder/decoder.

Resources

Stars

91 stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages