There are probably a lot better ways to do it, but take it as a learning exercise. Basically below is the JSON InputValidation and parsing using nlohmann::json
which takes expected fields, objects arrays and verifies its presence and (optionally) parses them into an appropriate c++ structure.
inputvalidation.hpp:
namespace iv { template<typename _Tp> class Field; template<typename... _Ts> class Object; template<typename _Tp> class Array; template<typename _Old, typename _New> class Deprecated; namespace detail { template<class _Tp, template<class...> class Template> struct is_specialization : ::std::false_type {}; template<template<class...> class Template, class... Args> struct is_specialization<Template<Args...>, Template> : ::std::true_type {}; template<typename _Tp> struct remove_opt { using type = _Tp; }; template<typename _Tp> struct remove_opt<::std::optional<_Tp>> { using type = _Tp; }; template<typename _Tp> using remove_opt_t = typename remove_opt<_Tp>::type; template<typename _Tp> using decay_t = ::std::decay_t<remove_opt_t<_Tp>>; #define _CONSTEVAL constexpr template<typename _pack, std::size_t N> _CONSTEVAL std::size_t elem_size(std::size_t& ref, std::array<std::size_t, std::tuple_size_v<_pack>>& offsets) noexcept { using _Tp = std::conditional_t< is_specialization<std::tuple_element_t<N, _pack>, std::optional>{}, std::optional<typename decay_t<std::tuple_element_t<N, _pack>>::value_type>, typename decay_t<std::tuple_element_t<N, _pack>>::value_type>; while (ref % alignof(_Tp) != 0) ++ref; offsets[N] = ref; ref += sizeof(_Tp); return alignof(_Tp); } template<typename _pack, typename std::size_t... Indices> _CONSTEVAL const std::tuple< const size_t, const size_t, const std::array<std::size_t, std::tuple_size_v<_pack>>> structure_type_helper(std::index_sequence<Indices...>) { std::size_t size = 0; std::array<std::size_t, std::tuple_size_v<_pack>> offsets = {}; auto pad = (elem_size<_pack, Indices>(size, offsets) | ...); std::size_t padding = 1; while (pad >>= 1) padding *= 2; return std::make_tuple(size, padding, offsets); } template<typename _Tp> struct structure_type { static constexpr const auto _storage = structure_type_helper<_Tp>(std::make_index_sequence<std::tuple_size_v<_Tp>>()); using type = typename std::aligned_storage_t<std::get<0>(_storage), std::get<1>(_storage)>; static constexpr const std::array<std::size_t, std::tuple_size_v<_Tp>>& offsets = std::get<2>(_storage); }; template<typename _Tp> using structure_type_t = typename structure_type<_Tp>::type; #undef _CONSTEVAL template<typename _pack, typename std::size_t... Indices> inline bool typeCheck(const nlohmann::json& j, const _pack& tuple, std::index_sequence<Indices...>) noexcept; template<typename _pack, typename std::size_t... Indices> inline void fromTuple(const _pack& tuple, const nlohmann::json& j, uint8_t* where, std::index_sequence<Indices...>); } template<typename _Tp> class Field { static_assert(!std::is_reference_v<_Tp> && !std::is_pointer_v<_Tp>, "Field type can not have a reference or a pointer type"); static_assert(!detail::is_specialization<_Tp, Field>{}, "Field type can not have field as a value type"); public: using value_type = _Tp; using comparator_type = bool(const value_type&); constexpr Field() = default; constexpr explicit Field(const char* tp) : _name(tp) {} constexpr explicit Field(const char* tp, comparator_type f) : _name(tp), _comp(f) {} bool check(const nlohmann::json& j) const noexcept { try { auto value = j.get<value_type>(); if (_comp) { return _comp(value); } return true; } catch (...) { return false; } } value_type parse(const nlohmann::json& j) const { return j.get<value_type>(); } constexpr const char* name() const noexcept { return _name; } private: const char* _name = nullptr; comparator_type* _comp = nullptr; }; template<typename... _Ts> class Object { static_assert(sizeof...(_Ts), "Object must have at least one field"); public: using tuple_type = std::tuple<_Ts...>; using value_type = typename detail::structure_type_t<tuple_type>; constexpr Object() = default; constexpr explicit Object(const char* tp, tuple_type&& fields) : _name(tp), _pack(std::move(fields)) {} constexpr explicit Object(const char* tp, const Object& ref) : _name(tp), _pack(ref._pack) {} bool check(const nlohmann::json& j) const noexcept { if (j.is_object() != true) { return false; } if constexpr (sizeof...(_Ts) != 0) { return detail::typeCheck(j, _pack, std::make_index_sequence<std::tuple_size_v<tuple_type>>()); } } value_type parse(const nlohmann::json& j) const { value_type storage; uint8_t* ptr = reinterpret_cast<uint8_t*>(&storage); detail::fromTuple(_pack, j, ptr, std::make_index_sequence<std::tuple_size_v<tuple_type>>()); return storage; } constexpr const char * name() const noexcept { return _name; } constexpr const tuple_type& pack() const noexcept { return _pack; } private: const char* _name = nullptr; tuple_type _pack; }; template<typename _Tp> class Array { static_assert(!std::is_reference_v<_Tp> && !std::is_pointer_v<_Tp>, "Can not create an array of pointers or references"); static_assert(!detail::is_specialization<_Tp, std::optional>{}, "Can not create an array of optionals"); public: using value_type = std::vector<typename _Tp::value_type>; constexpr Array() = default; constexpr explicit Array(const char* tp) : _name(tp) {} constexpr explicit Array(const char* tp, std::size_t limit) : _name(tp), _lim(limit) {} constexpr explicit Array(const char* tp, const _Tp& check, std::size_t limit = 0) : _name(tp), _comp(check), _lim(limit) {} bool check(const nlohmann::json& j) const noexcept { if (j.is_array() != true) { return false; } if (_lim && j.size() > _lim) { return false; } for (const auto& elem : j) { if (_comp.check(elem) != true) { return false; } } return true; } value_type parse(const nlohmann::json& j) const { value_type ret; ret.reserve(16); for (const auto& elem : j) { ret.push_back(_comp.parse(elem)); } return ret; } constexpr const char * name() const noexcept { return _name; } constexpr std::size_t limit() const noexcept { return _lim; } private: const char* _name = nullptr; _Tp _comp; std::size_t _lim = 0; }; template<typename _Old, typename _New> class Deprecated { static_assert(!detail::is_specialization<_Old, Deprecated>{} && !detail::is_specialization<_New, Deprecated>{}, "Deprecation of deprecated type is not allowed"); public: using depr_type = _Old; using new_type = _New; using value_type = std::variant<typename depr_type::value_type, typename new_type::value_type>; constexpr Deprecated() = default; constexpr explicit Deprecated(_Old&& depr, _New&& replacement) : _old(depr), _new(replacement) {} bool check(const nlohmann::json& j) const noexcept { return _new.check(j) || _old.check(j); } value_type parse(const nlohmann::json& j) const { return _new.check(j) ? _new.parse(j) : _old.parse(j); } constexpr const char * name() const noexcept { return _new.name(); } _Old _old; _New _new; }; namespace detail { #define _RUNTIME inline template<std::size_t N, class... _Ts> _RUNTIME const decay_t<std::tuple_element_t<N, std::tuple<_Ts...>>>& getVal(const std::tuple<_Ts...>& tuple) noexcept { if constexpr (is_specialization<std::decay_t<std::tuple_element_t<N, std::tuple<_Ts...>>>, std::optional>{}) { return std::get<N>(tuple).value(); } else { return std::get<N>(tuple); } } template<std::size_t N, class... _Ts> _RUNTIME bool typeCheckHelper(const nlohmann::json& j, const std::tuple<_Ts...>& tuple) noexcept { auto it = j.find(getVal<N>(tuple).name()); if (it == j.end() || it->is_null()) // element not found { if constexpr (is_specialization<std::decay_t<std::tuple_element_t<N, std::tuple<_Ts...>>>, std::optional>{}) { return true; } //TODO: Handle error - field not found return false; } if (getVal<N>(tuple).check(*it) == false) { //TODO: handle error - invalid field type return false; } return true; } template<typename _pack, typename std::size_t... Indices> _RUNTIME bool typeCheck(const nlohmann::json& j, const _pack& tuple, std::index_sequence<Indices...>) noexcept { return (typeCheckHelper<Indices>(j, tuple) && ...); } template<typename _Tp> _RUNTIME const decay_t<_Tp>& getVal(const _Tp& ref) { if constexpr (is_specialization<std::decay_t<_Tp>, std::optional>{}) { return ref.value(); } else { return ref; } } template<typename _Tp> _RUNTIME void fromTupleImpl(_Tp&& element, const nlohmann::json& data, uint8_t* where) { using _Ty = std::conditional_t< is_specialization<_Tp, std::optional>{}, std::optional<typename decay_t<_Tp>::value_type>, typename decay_t<_Tp>::value_type>; new (where) _Ty(getVal(element).parse(data[getVal(element).name()])); } template<typename _pack, typename std::size_t... Indices> _RUNTIME void fromTuple(const _pack& tuple, const nlohmann::json& j, uint8_t* where, std::index_sequence<Indices...>) { ((void)fromTupleImpl(std::get<Indices>(tuple), j, where + structure_type<_pack>::offsets[Indices]), ...); } #undef _RUNTIME } template<typename... _Ts> constexpr Object<_Ts...> make_object(const char* name, _Ts&& ...args) { return Object<_Ts...>{name, std::make_tuple(std::forward<decltype(args)>(args)...)}; } template<typename... _Ts> constexpr std::optional<Object<_Ts...>> make_nullable_object(const char* name, _Ts&& ...args) { return Object<_Ts...>{name, std::make_tuple(std::forward<decltype(args)>(args)...)}; } template<typename _Tp, typename... _Ts> constexpr _Tp get(const Object<_Ts...>& ref, const nlohmann::json& j) { static_assert(alignof(detail::structure_type_t<std::tuple<_Ts...>>) == alignof(_Tp) && alignof(detail::structure_type_t<std::tuple<_Ts...>>) == alignof(_Tp), "Invalidly calculated structure alignment and/or size."); auto _storage = ref.parse(j); return *reinterpret_cast<_Tp*>(&_storage); } }
Usage:
// this is 'read' from the file nlohmann::json j; j["first"] = 1; j["second"] = "string"; j["third"]["subfield1"] = "asdf"; j["third"]["subfield2"] = 1954; j["third"]["subfield3"].push_back(1); j["third"]["subfield3"].push_back(8); j["third"]["subfield3"].push_back(27); // structure metadata - tell the validator what do you expect in JSON auto obj = make_object("", Field<int>{"first"}, Field<std::string>{"second"}, make_object("third", Field<std::string>{"subfield1"}, Field<int>{"subfield2"}, Array<Field<double>>{"subfield3"} ) ); // create a structure that reflects the JSON layout struct s1 { int a; std::string b; struct { std::string a; int b; std::vector<double> c; } c; }; // verify that it has everything you're expecting and parse it if (obj.check(j)) { s1 s = get<s1>(obj, j); // do whatever you want with the structure }
You can also have an array of objects if you want. Go ahead and experiment if you want..
Side note: At the moment having std::vector of structure containing std::string have unexpected effects when accessing the string on clang and gcc. Works with MSVC tho. I don't know what the problem is unfortunately. I've track that to the std::vector itself so far.