概要
picojson は C++ で JSON を扱う際に便利なライブラリーだが、JSONの取り扱いにありがちなドット区切りのパス文字列で picojson::value
にネストして格納された picojson::object
へ用意にアクセスする機能が実装されていない事にしばしば不便する事がある。 †1
// 理想: picojson には実装されていないが時折欲しくなる使用例
const auto element_value = root_value.[ "aaa.bbb.ccc" ].as< double >();
// 現実: 毎度こんなに書きたくない
const double element_value
try
{
element_value =
root_value.get< picojson::object >().at( "aaa" )
.get< picojson::object >().at( "bbb" )
.get< picojson::object >().at( "ccc" )
.get< double >()
;
}
catch( ... )
{ ... }
そこで、ネストした picojson::object
構造を格納した picojson::value
からドット区切りのパス文字列で picojson::value
を引っ張り出すヘルパーライブラリーを作り、使用する。
必要な機能
- ドット区切りのパス文字列を階層ごとに分離する機能: boost::split †1
- 分離されたパスを取得する機能
- ユーザーの望む型で返す機能
実装詳細
基本: パス文字列で引っ張り出す部分
# include <picojson.h>
# include <boost/algorithm/string.hpp>
# include <boost/optional.hpp>
# include <vector>
namespace usagi::json::picojson
{
using object_type = ::picojson::object;
using value_type = ::picojson::value;
/// @brief value_type に対しドット区切りのパスで object_type の階層を辿り value_type を引っ張り出す
static inline auto get_value( const value_type& source, const std::string& dot_separated_path )
-> value_type&
{
std::vector< std::string > path;
boost::split( path, dot_separated_path, boost::is_any_of( "." ) );
auto out = const_cast< const value_type* >( &source );
for ( const auto& path_part : path )
out = &out->get< object_type >().at( path_part );
return const_cast< value_type& >( *out );
}
}
おまけ: optional で返す get_value
get_value
はいくつかの例外を発生する可能性がある。
-
picojson::value
をpicojson::object
に.get
に失敗する場合 ->std::runtime_error
-
picojson::object
に"aaa"
などのキーが存在せず.at
に失敗する場合 ->std::out_of_range
JSONを扱う場合、この様な例外は想定の範囲内の事はままある。そこで、そのような場合を予め想定し、 boost::optional
で結果を返すヘルパーも用意しておくと便利な事が多い。
/// @brief get_value が out_of_range や runtime_error など例外で失敗する場合に optional で例外の送出をカバーする版
static inline auto get_value_optional( const value_type& source, const std::string& dot_separated_path )
noexcept
-> boost::optional< value_type& >
{
try
{ return get_value( source, dot_separated_path ); }
catch ( ... )
{ return { }; }
}
応用: 欲しい型で取得できるようにする
picojson が扱う可能性のある型は以下の通り。
- picojson::value の中身の型の可能性
-
double
: JSON の数値 - `std::string : JSON の文字列
-
picojson::array
: JSON の配列 -
picojson::object
: JSON のオブジェクト(≃辞書) -
picojson::null
: JSONの null
-
C++ で扱いたい事の多い型は以下の通り。
- C++的に受け取りたい事の多い型
- 数値系
-
float
,double
,long double
: 浮動小数点数型 -
std::uint8_t
,std::uint16_t
,std::uint32_t
,std::uint64_t
: 非負整数型 -
std::int8_t
,std::int16_t
,std::int32_t
,std::int64_t
: 整数型
-
-
std::string
: 文字列型 -
std::vector
: 配列 -
std::unordered_map
: 辞書
- 数値系
特に数値として double
以外の型で扱いたい場合に picojson では一旦 double
で数値を取り出してから static_cast< int >
などする必要があるのでしばしば面倒。そこで、 get_value_as< int >
など便利に使えるヘルパーも用意したくなるので、 picojson::value
-> T
の取り出しパターンについて考える。
-
double
->double
-
double
->double
以外の数値型 -
string
->string
-
string
以外のpicojson::value
の中身の型 ->string
-
picojson::array
->picojson::array
(≃std::vector
) -
picojson::object
->picojson::object
(≃std::unordered_map
)
こんな程度の扱いができると事実上の便利としては困らなくなる。
/// @brief get_value + picojson::get + 可能な限りの自動的な型変換( double や string を float で取り出したり、 null を string で取り出したりもできる )
/// @param type_conversion true の場合には可能な限りの自動的な型変換を試みる。 false の場合には value_type::get のみ。
template < typename T >
static inline auto get_value_as
( const value_type& source
, const std::string& dot_separated_path
, const bool type_conversion = true
) -> T
{
const auto& v = get_value( source, dot_separated_path );
if ( type_conversion )
{
// T が数値型の場合の変換込みの処理
if ( std::is_integral< T >::value or std::is_floating_point< T >::value )
{
// double -> T
if ( v.is< double >() )
return static_cast< T >( v.get< double >() );
// string -> T
if ( v.is< std::string >() )
{
std::stringstream s;
s << v.get< std::string >();
T out;
s >> out;
if ( s.fail() )
throw std::runtime_error( "cannot convert to a number type from std::string type." );
return out;
}
}
}
throw std::runtime_error( "cannot convert to a number type from value_type type." );
}
template < >
inline auto get_value_as< double >
( const value_type& source
, const std::string& dot_separated_path
, const bool type_conversion
) -> double
{
const auto& v = get_value( source, dot_separated_path );
if ( v.is< double >() )
return v.get< double >();
if ( type_conversion )
{
// string -> double
if ( v.is< std::string >() )
{
std::stringstream s;
s << v.get< std::string >();
double out;
s >> out;
if ( s.fail() )
throw std::runtime_error( "cannot convert to a number type from std::string type." );
return out;
}
}
// note: picojson による cast 失敗で適当な std::runtime_error が発行される
return v.get< double >();
}
template < >
inline auto get_value_as< std::string >
( const value_type& source
, const std::string& dot_separated_path
, const bool type_conversion
) -> std::string
{
const auto& v = get_value( source, dot_separated_path );
if ( v.is< std::string >() )
return v.get< std::string >();
if ( type_conversion )
{
// value_type の operator<< で string にして返す
std::stringstream s;
s << v;
return s.str();
}
// note: picojson による cast 失敗で適当な std::runtime_error が発行される
return v.get< std::string >();
}
template < >
inline auto get_value_as< object_type >
( const value_type& source
, const std::string& dot_separated_path
, const bool type_conversion
) -> object_type
{
const auto& v = get_value( source, dot_separated_path );
// note: picojson による cast 失敗で適当な std::runtime_error が発行される
return v.get< object_type >();
}
template < >
inline auto get_value_as< array_type >
( const value_type& source
, const std::string& dot_separated_path
, const bool type_conversion
) -> array_type
{
const auto& v = get_value( source, dot_separated_path );
// note: picojson による cast 失敗で適当な std::runtime_error が発行される
return v.get< array_type >();
}
おまけ: optional で返す get_value_as
get_value
と同様に get_value_as
にも optional 版があると便利の良い事がままある。
/// @brief get_value_as が out_of_range や runtime_error など例外で失敗する場合に optional で例外の送出をカバーする版
template < typename T >
static inline auto get_value_as_optional
( const value_type& source
, const std::string& dot_separated_path
, const bool type_conversion = true
) noexcept -> boost::optional< T >
{
try
{ return get_value_as< T >( source, dot_separated_path, type_conversion ); }
catch ( ... )
{ return { }; }
}
ライブラリー実装例
ライブラリーとしてすぐに使えるように実装を整理すると次のようになる。
使用例
# include <usagi/json/picojson/get_value.hxx>
# include <iostream>
# include <iomanip>
auto main() -> int
{
using namespace std;
picojson::value v;
if ( const auto& e = picojson::parse
( v
, "{ 'aaa':"
" { 'bbb':"
" { 'ccc': 1.23"
" , 'ddd': '345.6789012345678e+9'"
" }"
"}"
)
)
throw runtime_error( e.what() );
cout << v << '\n';
using namespace usagi::json::picojson;
cout << get_value_as< double >( v, "aaa.bbb.ccc" ) << '\n';
cout << get_value_as< float >( v, "aaa.bbb.ccc" ) << '\n';
cout << get_value_as< int >( v, "aaa.bbb.ccc" ) << '\n';
cout << get_value_as< uint16_t >( v, "aaa.bbb.ccc" ) << '\n';
cout << to_string( get_value_as< uint8_t >( v, "aaa.bbb.ccc" ) ) << '\n';
cout << get_value_as< string >( v, "aaa.bbb.ccc" ) << '\n';
cout << get_value_as< picojson::object >( v, "aaa.bbb" ).at( "ccc" ) << '\n';
cout << setprecision( 10 ) << get_value_as< float >( v, "aaa.bbb.ddd" ) << '\n';
}
実行結果
{"aaa":{"bbb":{"ccc":1.23,"ddd":2.3399999141693115,"eee":"345.6789012345678e+9"}}}
1.23
1.23
1
1
1
1.23
1.23
3.456789053e+011