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 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 34254b0..624fc3b 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -29,7 +29,6 @@ #include #include - #if defined(_WIN32) #include #else @@ -4417,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; } @@ -4494,6 +4502,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); @@ -5722,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 @@ -6191,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 eb2e977..83198f2 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 + + public: + static const TJValue& null_value(); + private: template std::vector get_vector_internal(std::true_type) const @@ -820,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. @@ -1227,7 +1271,8 @@ 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; @@ -1484,7 +1529,8 @@ 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/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; +} diff --git a/tests/testtinyjsonarrays.cpp b/tests/testtinyjsonarrays.cpp index 9bd16e5..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()); @@ -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..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()); @@ -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; +} diff --git a/tests/testtinyjsonvaluesget.cpp b/tests/testtinyjsonvaluesget.cpp index bc2ab5f..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 = {}; @@ -346,3 +397,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); }