From 53eb618ae00c91b891e22336dd38beaaf36eb9cc Mon Sep 17 00:00:00 2001 From: FFMG Date: Wed, 20 May 2026 16:52:38 +0200 Subject: [PATCH 1/6] Removed space --- src/TinyJSON.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TinyJSON.cpp b/src/TinyJSON.cpp index 34254b0..863d2d7 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -29,7 +29,6 @@ #include #include - #if defined(_WIN32) #include #else From 8ed59a4acf19c15e8b8c65dc31650230fde40104 Mon Sep 17 00:00:00 2001 From: FFMG Date: Wed, 20 May 2026 17:24:52 +0200 Subject: [PATCH 2/6] Added operator[] --- README.md | 27 +++++++++++++++-- src/TinyJSON.cpp | 33 ++++++++++++++++++++ src/TinyJSON.h | 35 ++++++++++++++++++++-- tests/testtinyjsonvaluesget.cpp | 53 +++++++++++++++++++++++++++++++++ tests/testtinyjsonversion.cpp | 4 +-- 5 files changed, 145 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ddba4b9..0e57278 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-0.2.3-blue.svg)](src/TinyJSON.h) +[![Version](https://img.shields.io/badge/version-0.2.4-blue.svg)](src/TinyJSON.h) A lightweight and lightning-fast C++ JSON & JSON5 parser designed for high performance and minimal footprint. @@ -19,6 +19,7 @@ A lightweight and lightning-fast C++ JSON & JSON5 parser designed for high perfo - [Data types](#data-types) - [Simple examples](#simple-examples) - [Version Control](#version-control) + - [Simple Value Access](#simple-value-access) - [Options](#options) - [Exceptions](#exceptions) - [Check if JSON is valid](#check-if-json-is-valid) @@ -130,8 +131,28 @@ The version is set in the `TinyJSON.h` file. ```cpp static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 3; -static const char TJ_VERSION_STRING[] = "0.2.3"; +static const short TJ_VERSION_PATCH = 4; +static const char TJ_VERSION_STRING[] = "0.2.4"; +``` + +### Simple Value Access + +TinyJSON provides a convenient `operator[]` for object member access and a templated `as()` method for casting values. + +```cpp +auto* tj = TJ::parse(R"({"a": {"b": 10}})"); +if (tj) { + // Access nested members directly + int value = (*tj)["a"]["b"].as(); // 10 + + // Handling missing keys (behavior depends on parse_options::throw_exception) + // If throw_exception is false (default), it returns a null value + if ((*tj)["missing"].is_null()) { + // ... + } + + delete tj; +} ``` ## Options diff --git a/src/TinyJSON.cpp b/src/TinyJSON.cpp index 863d2d7..2137808 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -4493,6 +4493,39 @@ namespace TinyJSON _parse_options = options; } + const TJValue& TJValue::null_value() + { + static const TJValueNull val; + return val; + } + + const TJValue& TJValue::operator[](const TJCHAR* key) const + { + if (is_object()) + { + const TJValueObject* obj = static_cast(this); + const TJValue* val = obj->try_get_value(key); + if (val != nullptr) + { + return *val; + } + } + + if (_parse_options.throw_exception) + { + throw TJParseException("Key not found"); + } + + return null_value(); + } + +#if TJ_INCLUDE_STD_STRING == 1 + const TJValue& TJValue::operator[](const std::string& key) const + { + return (*this)[key.c_str()]; + } +#endif + void TJValueObject::set_parse_options(const parse_options& options) { TJValue::set_parse_options(options); diff --git a/src/TinyJSON.h b/src/TinyJSON.h index eb2e977..2b08c30 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -45,10 +45,11 @@ // v0.2.1 - added remove_at to TJValueArray. // v0.2.2 - added support for Json5 https://github.com/json5/ // v0.2.3 - added atomic file saving +// v0.2.4 - added operator[] and as() accessors to TJValue static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 3; -static const char TJ_VERSION_STRING[] = "0.2.3"; +static const short TJ_VERSION_PATCH = 4; +static const char TJ_VERSION_STRING[] = "0.2.4"; #ifndef TJ_USE_CHAR # define TJ_USE_CHAR 1 @@ -671,6 +672,36 @@ class TJDictionary; return get_vector_internal(std::is_integral()); } + /// + /// Casting the value to a specific type. + /// it behaves exactly as the get() methods. + /// + /// + template + auto as() const -> decltype(this->get()) + { + return get(); + } + + /// + /// Access a member of an object by its key. + /// + /// + /// + const TJValue& operator[](const TJCHAR* key) const; + +#if TJ_INCLUDE_STD_STRING == 1 + /// + /// Access a member of an object by its key. + /// + /// + /// + const TJValue& operator[](const std::string& key) const; +#endif + + private: + static const TJValue& null_value(); + private: template std::vector get_vector_internal(std::true_type) const diff --git a/tests/testtinyjsonvaluesget.cpp b/tests/testtinyjsonvaluesget.cpp index bc2ab5f..b8b9b27 100644 --- a/tests/testtinyjsonvaluesget.cpp +++ b/tests/testtinyjsonvaluesget.cpp @@ -346,3 +346,56 @@ TEST(TestValueGet, GetStrictStringFromArrayWillThrow) delete json; } + +TEST(TestValueGet, OperatorBracketSimpleAccess) +{ + auto json = TinyJSON::TJ::parse(R"({"a": {"b": 10}, "c": [1, 2, 3]})"); + ASSERT_NE(nullptr, json); + + // Simple access + ASSERT_EQ(10, (*json)["a"]["b"].as()); + + delete json; +} + +TEST(TestValueGet, OperatorBracketMissingKeyNoThrow) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = false; + auto json = TinyJSON::TJ::parse(R"({"a": {"b": 10}})", options); + ASSERT_NE(nullptr, json); + + // Missing key (no throw) returns null value + ASSERT_TRUE((*json)["a"]["x"].is_null()); + ASSERT_TRUE((*json)["x"]["y"].is_null()); // Non-existent subkey on null fallback + + // as default on null (TJValueNull::get_int() returns 0) + ASSERT_EQ(0, (*json)["a"]["x"].as()); + + delete json; +} + +TEST(TestValueGet, OperatorBracketThrowOnMissingKey) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + auto json = TinyJSON::TJ::parse(R"({"a": {"b": 10}})", options); + ASSERT_NE(nullptr, json); + + ASSERT_EQ(10, (*json)["a"]["b"].as()); + ASSERT_ANY_THROW((*json)["a"]["x"]); + ASSERT_ANY_THROW((*json)["x"]); + + delete json; +} + +TEST(TestValueGet, OperatorBracketOnNonObject) +{ + auto json = TinyJSON::TJ::parse(R"([1, 2, 3])"); // It is an array, not an object + ASSERT_NE(nullptr, json); + + // Accessing by key on a non-object should return null (or throw if configured) + ASSERT_TRUE((*json)["key"].is_null()); + + delete json; +} diff --git a/tests/testtinyjsonversion.cpp b/tests/testtinyjsonversion.cpp index 49b0971..effb28a 100644 --- a/tests/testtinyjsonversion.cpp +++ b/tests/testtinyjsonversion.cpp @@ -14,9 +14,9 @@ TEST(TestVersion, CheckVersionMinor) { } TEST(TestVersion, CheckVersionPatch) { - ASSERT_EQ(3, TJ_VERSION_PATCH); + ASSERT_EQ(4, TJ_VERSION_PATCH); } TEST(TestVersion, CheckVersionString) { - ASSERT_STREQ("0.2.3", TJ_VERSION_STRING); + ASSERT_STREQ("0.2.4", TJ_VERSION_STRING); } From 2727640458e626d16fade015cc361424a26cace1 Mon Sep 17 00:00:00 2001 From: FFMG Date: Wed, 20 May 2026 17:25:34 +0200 Subject: [PATCH 3/6] Updated change log --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2377fb1..9f840ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.4] - 2026-05-20 + +### Added +- Dictionary-style member access via `operator[]` for `TJValue`. +- Templated `as()` helper method for simplified type casting (e.g., `json["key"].as()`). + +### Changed +- Incremented version to 0.2.4. + ## [0.2.3] - 2026-05-19 ### Added From 213cf8f8b530808982d4a9a1befb416ac0a47a56 Mon Sep 17 00:00:00 2001 From: FFMG Date: Wed, 20 May 2026 18:44:25 +0200 Subject: [PATCH 4/6] Added more tests to cover different types --- tests/testtinyjsonarrays.cpp | 13 ++++++++++++- tests/testtinyjsonbooleans.cpp | 13 ++++++++++++- tests/testtinyjsonnulls.cpp | 13 ++++++++++++- tests/testtinyjsonnumbers.cpp | 13 ++++++++++++- tests/testtinyjsonobjects.cpp | 12 ++++++++++++ tests/testtinyjsonset.cpp | 9 +++++++++ tests/testtinyjsonstrings.cpp | 11 +++++++++++ 7 files changed, 80 insertions(+), 4 deletions(-) diff --git a/tests/testtinyjsonarrays.cpp b/tests/testtinyjsonarrays.cpp index 9bd16e5..03afbbe 100644 --- a/tests/testtinyjsonarrays.cpp +++ b/tests/testtinyjsonarrays.cpp @@ -558,4 +558,15 @@ TEST(TJValueArray, AddVectorOfNumberAndGetItAsAVector) ASSERT_EQ(4, sum); // 1 + 3 delete json; - } \ No newline at end of file + } + +TEST(TJValueArray, OperatorBracketAccess) +{ + auto json = TinyJSON::TJ::parse(R"([1, 2, 3])"); + ASSERT_NE(nullptr, json); + + // operator[] on array should return null (it is for object keys) + ASSERT_TRUE((*json)["any_key"].is_null()); + + delete json; +} diff --git a/tests/testtinyjsonbooleans.cpp b/tests/testtinyjsonbooleans.cpp index fcb89ad..06afcc1 100644 --- a/tests/testtinyjsonbooleans.cpp +++ b/tests/testtinyjsonbooleans.cpp @@ -225,4 +225,15 @@ TEST(TestBooleans, CloneFalse) { delete bool1; delete bool2; -} \ No newline at end of file +} + +TEST(TestBooleans, OperatorBracketAccess) +{ + auto json = TinyJSON::TJ::parse("true"); + ASSERT_NE(nullptr, json); + + // operator[] on boolean should return null + ASSERT_TRUE((*json)["any_key"].is_null()); + + delete json; +} diff --git a/tests/testtinyjsonnulls.cpp b/tests/testtinyjsonnulls.cpp index 116d395..ed5c610 100644 --- a/tests/testtinyjsonnulls.cpp +++ b/tests/testtinyjsonnulls.cpp @@ -170,4 +170,15 @@ TEST(TestNulls, CloneNull) { delete null1; delete null2; -} \ No newline at end of file +} + +TEST(TestNulls, OperatorBracketAccess) +{ + auto json = TinyJSON::TJ::parse("null"); + ASSERT_NE(nullptr, json); + + // operator[] on null should return null + ASSERT_TRUE((*json)["any_key"].is_null()); + + delete json; +} diff --git a/tests/testtinyjsonnumbers.cpp b/tests/testtinyjsonnumbers.cpp index 70f0e4f..dac2698 100644 --- a/tests/testtinyjsonnumbers.cpp +++ b/tests/testtinyjsonnumbers.cpp @@ -514,4 +514,15 @@ TEST(TestNumbers, CreateFloatByPassingNegativeNumber) { ASSERT_EQ(-42, value->get_number()); ASSERT_EQ(-42.5, value->get_float()); delete json; -} \ No newline at end of file + } + + TEST(TestNumbers, OperatorBracketAccess) + { + auto json = TinyJSON::TJ::parse("123"); + ASSERT_NE(nullptr, json); + + // operator[] on number should return null + ASSERT_TRUE((*json)["any_key"].is_null()); + + delete json; + } diff --git a/tests/testtinyjsonobjects.cpp b/tests/testtinyjsonobjects.cpp index abeb183..1a39b64 100644 --- a/tests/testtinyjsonobjects.cpp +++ b/tests/testtinyjsonobjects.cpp @@ -1257,3 +1257,15 @@ TEST(TestObjects, NumberOfItemsUpdatesOnMutation) { delete json; } + +TEST(TestObjects, OperatorBracketAccess) +{ + auto json = TinyJSON::TJ::parse(R"({"key": "value", "nested": {"a": 1}})"); + ASSERT_NE(nullptr, json); + + // Test operator[] for objects + ASSERT_STREQ("value", (*json)["key"].as()); + ASSERT_EQ(1, (*json)["nested"]["a"].as()); + + delete json; +} diff --git a/tests/testtinyjsonset.cpp b/tests/testtinyjsonset.cpp index 3c1d345..4638d07 100644 --- a/tests/testtinyjsonset.cpp +++ b/tests/testtinyjsonset.cpp @@ -79,3 +79,12 @@ TEST(TestSet, GenericSetVectorFloat) { ASSERT_NEAR(1.1, result[0], 0.001); ASSERT_NEAR(2.2, result[1], 0.001); } + +TEST(TestSet, OperatorBracketAccess) +{ + TinyJSON::TJValueObject obj; + obj.set("a", 42); + + ASSERT_EQ(42, obj["a"].as()); + ASSERT_TRUE(obj["b"].is_null()); +} diff --git a/tests/testtinyjsonstrings.cpp b/tests/testtinyjsonstrings.cpp index 4190fce..18565b4 100644 --- a/tests/testtinyjsonstrings.cpp +++ b/tests/testtinyjsonstrings.cpp @@ -776,3 +776,14 @@ TEST(TestStrings, Json5AllowsEscapedDoubleQuotes) { ASSERT_STREQ(json->get_string(), "I can use quotes\""); delete json; } + +TEST(TestStrings, OperatorBracketAccess) +{ + auto json = TinyJSON::TJ::parse(R"("hello")"); + ASSERT_NE(nullptr, json); + + // operator[] on string should return null + ASSERT_TRUE((*json)["any_key"].is_null()); + + delete json; +} From bf3425e0132fb29d5213dab41ab34d22095f7e8a Mon Sep 17 00:00:00 2001 From: FFMG Date: Wed, 20 May 2026 18:46:35 +0200 Subject: [PATCH 5/6] Added some json5 tests and tests --- src/TinyJSON.h | 2 ++ tests/testjson5objects.cpp | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/TinyJSON.h b/src/TinyJSON.h index 2b08c30..4cde59f 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -1259,6 +1259,7 @@ class TJDictionary; #endif TJMember* operator [](int idx) const; + using TJValue::operator[]; TJMember* at(int idx) const; TJMember* element_at(int idx) const; @@ -1516,6 +1517,7 @@ class TJDictionary; unsigned int get_number_of_elements() const; TJValue* operator [](int idx) const; + using TJValue::operator[]; TJValue* at(int idx) const; TJValue* element_at(int idx) const; diff --git a/tests/testjson5objects.cpp b/tests/testjson5objects.cpp index ceb932a..182c903 100644 --- a/tests/testjson5objects.cpp +++ b/tests/testjson5objects.cpp @@ -74,3 +74,21 @@ TEST(TJJSON5Objects, IllegalUnquotedKeyStartRejected) auto* tj = TJ::parse(json, options); ASSERT_EQ(nullptr, tj); } + +TEST(TJJSON5Objects, OperatorBracketJSON5Features) +{ + const TJCHAR* json = TJCHARPREFIX("{unquoted: 1, 'single': 2, while: true, $: 3, _: 4}"); + parse_options options; + options.specification = parse_options::json5_1_0_0; + auto* tj = TJ::parse(json, options); + ASSERT_NE(nullptr, tj); + + // Test operator[] with JSON5 specific key styles + ASSERT_EQ(1, (*tj)["unquoted"].as()); + ASSERT_EQ(2, (*tj)["single"].as()); + ASSERT_TRUE((*tj)["while"].as()); + ASSERT_EQ(3, (*tj)["$"].as()); + ASSERT_EQ(4, (*tj)["_"].as()); + + delete tj; +} From f00664d213bc29e9b73ba009b332928446f10215 Mon Sep 17 00:00:00 2001 From: FFMG Date: Wed, 20 May 2026 19:40:37 +0200 Subject: [PATCH 6/6] Added operator[]( int) .as() for objects and arrays. --- src/TinyJSON.cpp | 21 ++++++++++---- src/TinyJSON.h | 19 ++++++++++-- tests/testtinyjsonarrays.cpp | 2 +- tests/testtinyjsonobjects.cpp | 2 +- tests/testtinyjsonvaluesget.cpp | 51 +++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/TinyJSON.cpp b/src/TinyJSON.cpp index 2137808..624fc3b 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -4416,9 +4416,18 @@ namespace TinyJSON _string = nullptr; } + const TJMember& TJMember::null_member() + { + static const TJMember member(nullptr, &TJValue::null_value()); + return member; + } + void TJMember::free_value() { - delete _value; + if (nullptr != _value && _value != &TJValue::null_value()) + { + delete _value; + } _value = nullptr; } @@ -5754,9 +5763,10 @@ namespace TinyJSON return _members == nullptr ? 0 : _members->size(); } - TJMember* TJValueObject::operator [](int idx) const + const TJMember& TJValueObject::operator [](int idx) const { - return at(idx); + const TJMember* member = at(idx); + return member != nullptr ? *member : TJMember::null_member(); } TJMember* TJValueObject::at(int idx) const @@ -6223,9 +6233,10 @@ namespace TinyJSON return _values == nullptr ? 0 : _values->size(); } - TJValue* TJValueArray::operator [](int idx) const + const TJValue& TJValueArray::operator [](int idx) const { - return at(idx); + const TJValue* val = at(idx); + return val != nullptr ? *val : TJValue::null_value(); } TJValue* TJValueArray::at(int idx) const diff --git a/src/TinyJSON.h b/src/TinyJSON.h index 4cde59f..83198f2 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -699,7 +699,7 @@ class TJDictionary; const TJValue& operator[](const std::string& key) const; #endif - private: + public: static const TJValue& null_value(); private: @@ -851,6 +851,19 @@ class TJDictionary; const TJValue* value() const; TJValue* value(); + /// + /// Casting the value to a specific type. + /// it behaves exactly as the get() methods. + /// + /// + template + auto as() const -> decltype(this->value()->template as()) + { + return value()->template as(); + } + + static const TJMember& null_member(); + protected: /// /// Move the value ownership to the member. @@ -1258,7 +1271,7 @@ class TJDictionary; } #endif - TJMember* operator [](int idx) const; + const TJMember& operator [](int idx) const; using TJValue::operator[]; TJMember* at(int idx) const; TJMember* element_at(int idx) const; @@ -1516,7 +1529,7 @@ class TJDictionary; /// unsigned int get_number_of_elements() const; - TJValue* operator [](int idx) const; + const TJValue& operator [](int idx) const; using TJValue::operator[]; TJValue* at(int idx) const; TJValue* element_at(int idx) const; diff --git a/tests/testtinyjsonarrays.cpp b/tests/testtinyjsonarrays.cpp index 03afbbe..344c9eb 100644 --- a/tests/testtinyjsonarrays.cpp +++ b/tests/testtinyjsonarrays.cpp @@ -110,7 +110,7 @@ TEST(TJValueArray, EmptyArrayInSideArrayHasNoItemsInIt) { ASSERT_NE(nullptr, jarray); ASSERT_EQ(1, jarray->get_number_of_items()); - const auto jarraya = dynamic_cast((*jarray)[0]); + const auto jarraya = dynamic_cast(jarray->at(0)); ASSERT_NE(nullptr, jarraya); ASSERT_EQ(0, jarraya->get_number_of_items()); diff --git a/tests/testtinyjsonobjects.cpp b/tests/testtinyjsonobjects.cpp index 1a39b64..25a6da0 100644 --- a/tests/testtinyjsonobjects.cpp +++ b/tests/testtinyjsonobjects.cpp @@ -67,7 +67,7 @@ TEST(TestObjects, GetItemByIndex) { ASSERT_NE(nullptr, jobject); ASSERT_EQ(1, jobject->get_number_of_items()); - const auto jobjecta = dynamic_cast((*jobject)[0]->value()); + const auto jobjecta = dynamic_cast(jobject->at(0)->value()); ASSERT_NE(nullptr, jobjecta); ASSERT_EQ(1, jobjecta->get_number_of_items()); diff --git a/tests/testtinyjsonvaluesget.cpp b/tests/testtinyjsonvaluesget.cpp index b8b9b27..0c52d86 100644 --- a/tests/testtinyjsonvaluesget.cpp +++ b/tests/testtinyjsonvaluesget.cpp @@ -11,6 +11,57 @@ #include +TEST(TestValueGet, ArrayIndexAsInt) +{ + auto json = TinyJSON::TJ::parse("[10, 20, 30]"); + ASSERT_NE(nullptr, json); + auto jarray = dynamic_cast(json); + ASSERT_EQ(10, (*jarray)[0].as()); + ASSERT_EQ(20, (*jarray)[1].as()); + ASSERT_EQ(30, (*jarray)[2].as()); + delete json; +} + +TEST(TestValueGet, ArrayIndexOutOfBoundsNoThrow) +{ + auto json = TinyJSON::TJ::parse("[10, 20, 30]"); + ASSERT_NE(nullptr, json); + auto jarray = dynamic_cast(json); + ASSERT_TRUE((*jarray)[99].is_null()); + delete json; +} + +TEST(TestValueGet, ObjectIndexAsInt) +{ + auto json = TinyJSON::TJ::parse(R"({"a": 1, "b": 2})"); + ASSERT_NE(nullptr, json); + auto jobj = dynamic_cast(json); + // Objects are numbered values too + ASSERT_EQ(1, (*jobj)[0].as()); + ASSERT_EQ(2, (*jobj)[1].as()); + delete json; +} + +TEST(TestValueGet, ObjectIndexOutOfBoundsNoThrow) +{ + auto json = TinyJSON::TJ::parse(R"({"a": 1, "b": 2})"); + ASSERT_NE(nullptr, json); + auto jobj = dynamic_cast(json); + ASSERT_TRUE((*jobj)[99].value()->is_null()); + delete json; +} + +TEST(TestValueGet, MemberAsInt) +{ + auto json = TinyJSON::TJ::parse(R"({"a": 100})"); + ASSERT_NE(nullptr, json); + auto obj = dynamic_cast(json); + ASSERT_NE(nullptr, obj); + // Directly test TJMember::as() + ASSERT_EQ(100, (*obj)[0].as()); + delete json; +} + TEST(TestValueGet, GetBoolean) { TinyJSON::parse_options options = {};