LoginSignup
8
5

More than 5 years have passed since last update.

picojson::value に入った picojson::object のネスト構造をドット区切りのパス文字列で引っ張り出すヘルパー

Posted at

概要

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 を引っ張り出すヘルパーライブラリーを作り、使用する。

必要な機能

  1. ドット区切りのパス文字列を階層ごとに分離する機能: boost::split †1
  2. 分離されたパスを取得する機能
  3. ユーザーの望む型で返す機能

実装詳細

基本: パス文字列で引っ張り出す部分

#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::valuepicojson::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 の取り出しパターンについて考える。

  1. double -> double
  2. double -> double 以外の数値型
  3. string -> string
  4. string 以外の picojson::value の中身の型 -> string
  5. picojson::array -> picojson::array (≃ std::vector
  6. 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

References

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5