TECHNOLOGY

FortranでJSONファイルの入出力を行う方法(json-fortran)

FortranでJSONを扱う方法を探していたところ、gitにJSONパーサーがあったので試してみました。

その使い方をご紹介いたします。

目次

  1. JSONパーサーのダウンロード
  2. プログラムの構成
    1. 基本構成
    2. データの種類
  3. 使い方(JSONファイルの読込)
  4. 使い方(JSONファイルの出力)
    1. オブジェクトの作成
    2. データの追加
    3. オブジェクトのネスト
  5. まとめ

JSONパーサーのダウンロード

githubで公開されているfortranで記述されたJSONのパーサー(json-fortran)を使用します。githubから以下のコマンドで関連ファイルを入手します。

$ git clone https://github.com/jacobwilliams/json-fortran

lsコマンドを打つと、以下の構成になっています。

$ cd json-fortran
$ ls
CHANGELOG.md    build.sh     fpm.toml                     json-fortran.pc.cmake.in  visual_studio
CMakeLists.txt  cmake        json-fortran.code-workspace  media
LICENSE         codecov.yml  json-fortran.fobis           pages
README.md       files        json-fortran.md              src

ディレクトリsrcの中にfortranのソースコードが含まれています。また、ディレクトリfilesの中にはJSONファイルのサンプルが含まれています。json-fortranを使用するには、srcの中のソースコードのみが必要です。

$ cd src
$ ls
json_file_module.F90            json_initialize_arguments.inc        json_module.F90            tests
json_get_scalar_by_path.inc     json_initialize_dummy_arguments.inc  json_parameters.F90
json_get_vec_by_path.inc        json_kinds.F90                       json_string_utilities.F90
json_get_vec_by_path_alloc.inc  json_macros.inc                      json_value_module.F90

使用するためにコンパイルして実行体を作成します。ここでは、Intel oneAPIのifortを使用しました。gfortranでもコンパイル可能です。

コンパイルに用いるmakefileを以下のように作成し、ディレクトリsrcの中に配置しました。

prog:=test

main_src:=./tests/jf_test_01.F90

json_src:= json_kinds.F90          \
	       json_parameters.F90       \
	       json_string_utilities.F90 \
	       json_value_module.F90     \
	       json_file_module.F90      \
	       json_module.F90

json_objs:=$(json_src:%.F90=%.o)

opt:=-O2

all: $(prog)
	ifort $(opt) -o $(prog) $(main_src) -ljson -L./lib -I./include
	@#gfortran $(opt) -o $(prog) $(main_src) -ljson -L./lib -I./include

$(prog):
	@-mkdir -p include
	@-mkdir -p lib
	ifort $(opt) -c $(json_src) -module ./include
	@#gfortran $(opt) -c $(json_src) -J ./include
	ar cr libjson.a $(json_objs)
	mv libjson.a ./lib

clean:
	rm -rf *.a *.o *.mod lib include $(prog)

gfortranを使用する場合はifortの行をコメントアウトして、gfortranの行のコメントアウトを外してください。makeコマンドを実行すると、デフォルトではテスト用ファイル./tests/jf_test_01.F90がコンパイルされて、実行体testが作成されます。

$ make

コンパイルするファイルを変更する場合には、main_srcにメインプログラムのファイル名を設定します。例えば、テスト用ファイルjf_test_02.F90を実行する場合には以下のようにします。

$ make main_src=./tests/jf_test_02.F90

また、作成されたファイルを削除して元の状態に戻したい場合には、

$ make clean

とします。

以上で、JSONパーサーを用いて色々試してみる環境ができました。

具体的には、test.f90などの適当な名前のファイルを作成し、main_srcにその名前を与えれば、動作確認を行うことができます。

プログラムの構成

基本構成

構造体を定義し、initialize()で初期化、destroy()で破棄します。初期化と破棄の間に処理したい内容を記述します。

! JSONファイルを作成する場合
program main
    use json_module
    implicit none

    type(json_core) :: json
    call json%initialize()
    ! ここに処理を記述
    call json%destroy()
end program 

! JSONファイルを読み込む場合
program main
    use json_module
    implicit none

   ! JSONファイルを読み込む場合
    type(json_file) :: jsonfile
    call jsonfile%initialize()
    call jsonfile%load(filename = 'sample.json')
    ! ここに処理を記述
    call jsonfile%destroy()
end program 

データの種類

json-fortranのjson_kindsモジュールの中で、データ型のメモリサイズ(kind)が定義されています。それをjson_moduleの中で別名(alias)に変更しています。

下の表にその概要をまとめます。詳細はjson_kindsモジュールをご確認ください。

データタイプjson_module内での変数名json_kindsモジュール内での変数名参考
整数型json_IKIKデフォルトではint32(4 bytes)が使用されるが、コンパイルオプションにより変更できる。
例えば、int64(8 bytes)を使用する場合には、以下のオプションを追加する。
-DINT64
実数型json_RKRKデフォルトではreal64(8 bytes)が使用されるが、コンパイルオプションにより変更できる。
例えば、real128(16 bytes)を使用する場合には、以下のオプションを追加する。
-DREAL128
論理型json_LKLKデフォルトのlogical kindが使用される。
IntelとGfortranコンパイラでは 4 bytes である。
文字列型json_CKCKデフォルトではプロセッサのDEFAULTのcharacter kindが使用される。
IntelとGfortranコンパイラでは 1 byte である。
ISO 10646 (UCS4) をサポートしている場合には、コンパイル時にgfortranのコンパイルオプションに
-D__GFORTRAN__ -DUSE_UCS4
を追加することでISO 10646のサポートを使用できる。
この場合は4 bytesになる。このサポートは、gfortran >= 4.9.2で使用できる。

使い方(JSONファイルの読込)

JSONファイルからデータを読み込む方法について示します。

読み込みのテストとして、次のJSONファイルを使用します(ファイル名はsample.jsonとします)。

{
    ! 色々なデータタイプを含むオブジェクト
    "data1" : {
        "int" : 3,
        "int array" : [3, 4, 5],
        "real" : 1.0d0,
        "real array" : [4.0d0, 6.0d0, 8.0d0],
        "char" : "hoge",
        "char array" : ["hoge1", "hoge2", "hoge3"],
        "logical" : true,
        "logical array" : [true, false, true]
    },
    ! オブジェクトの中にオブジェクトの配列がある場合
    "data2" : [        
        {
           "no" : 1
        },
        {  
           "no" : 2
        }
    ]
}

JSONファイルからデータを読み込むには、get関数を使用します。get関数の第1引数には読み込みたいデータのタグを、第2引数には読み込んだ値を格納する変数を指定します。

タグは、パスセパレーター(デフォルトではドット(.))でオブジェクト名と変数名を接続することで作成します。

また、オブジェクトの中に配列がある場合には、名前の後ろに括弧()をつけ、その中に要素番号を入れることでタグを作成します。

以下に、上記のサンプルのJSONファイルを読み込み、標準出力するプログラムを記述します。

program main
    use json_module
    use json_module, IK => json_IK, &
                     RK => json_RK, &
                     CK => json_CK, &
                     LK => json_LK
    implicit none
    type(json_file) :: jsonfile       
    integer, parameter :: io_unit = 6                ! 標準出力用の装置番号

    ! 読み込んだ値を格納する変数
    integer(IK) :: ival                              ! 整数型
    integer(IK), allocatable :: ia(:)                ! 整数型配列
    real(RK) :: rval                                 ! 実数型
    real(RK), allocatable :: ra(:)                   ! 実数型配列
    character(kind=CK,len=:), allocatable :: cval    ! 文字列
    character(kind=CK,len=:), allocatable :: ca(:)   ! 文字列型配列
    integer(IK), allocatable :: icalen(:)            ! 文字列型配列の各文字列の長さを格納
    logical(LK) :: lval                              ! 論理型
    logical(LK), allocatable :: la(:)                ! 論理型配列

    ! ファイルの読込
    call jsonfile%initialize()
    call jsonfile%load(filename = 'sample.json')
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    endif

    ! データの読込と標準出力
    ! 整数の場合
    call jsonfile%get("data1.int", ival)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.int = ', ival
    endif

    ! 整数配列の場合
    call jsonfile%get('data1.int array', ia)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.int array = ', ia
    endif

    ! 実数の場合
    call jsonfile%get('data1.real', rval)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.real = ', rval
    endif

    ! 実数配列の場合
    call jsonfile%get('data1.real array', ra)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.real array = ', ra
    endif

    ! 文字列の場合
    call jsonfile%get('data1.char', cval)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.char = ', cval
    endif

    ! 文字列配列の場合
    call jsonfile%get('data1.char array', ca, icalen)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.char array = ', ca(1)//' '//ca(2)//' '//ca(3)
    endif

    ! 論理型変数の場合
    call jsonfile%get('data1.logical', lval)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.logical = ', lval
    endif

    ! 論理型配列の場合
    call jsonfile%get('data1.logical array', la)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data1.logical array = ', la
    endif

    ! オブジェクト内に配列がある場合
    call jsonfile%get('data2(1).no', ival)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data2(1).no = ', ival
    endif
    call jsonfile%get('data2(2).no', ival)
    if(jsonfile%failed()) then
        call jsonfile%print_error_message(io_unit)
        stop
    else 
        write(io_unit, *) 'data2(2).no = ', ival
    endif

    call jsonfile%destroy()
end program    

このプログラム(test.f90)を、

$ make main_src=test.f90
$ ./test

によりコンパイルして実行すると、以下の結果が得られます。

 data1.int =            3
 data1.int array =            3           4           5
 data1.real =    1.00000000000000
 data1.real array =    4.00000000000000  6.00000000000000   8.00000000000000
 data1.char = hoge
 data1.char array = hoge1 hoge2 hoge3
 data1.logical =  T
 data1.logical array =  T F T
 data2(1).no =            1
 data2(2).no =            2

以上により、FortranでJSONファイルを読み込むことができました。

使い方(JSONファイルの出力)

JSONファイルをFortranで作成する方法を示します。

オブジェクトの作成

jsonのオブジェクトの作成はcreate_object()で行います。第1引数にjson_value構造体のポインタ、第2引数にオブジェクトの名前を与えます。次の例ではオブジェクト名が空のrootオブジェクト、つまり、ただの中括弧{}を作成します。

program main
    use json_module
    implicit none
    type(json_core) :: json
    type(json_value), pointer :: root ! rootオブジェクト用のポインタ
    integer, parameter :: io_unit = 6 ! 標準出力に使用
    call json%initialize()
    ! rootオブジェクトを作成
    call json%create_object(root, '')
    ! 作成したオブジェクトを標準出力
    call json%print(root, io_unit)
    ! 作成したオブジェクトをファイル出力
    call json%print(root, 'result.json')
    call json%destroy()
end program

結果として、以下の内容のJSONファイルの作成と標準出力が行われます。

{}

データの追加

add()によりオブジェクトにデータを追加します。第1引数にデータを追加するオブジェクト、第2引数にキー、第3引数に値を設定します。値は通常の変数の他、配列やブラケットを用いて与えることもできます。

program main
    use json_module
    use json_module, IK => json_IK ! JSON-fortranで使用される整数型のメモリサイズ(kind)
    implicit none
    type(json_core) :: json
    type(json_value), pointer :: root ! rootオブジェクト用のポインタ
    integer, parameter :: io_unit = 6 ! 標準出力に使用
    integer(IK) :: int_scalar = 0 ! 追加するデータ
    integer(IK) :: int_vector(3) = (/1, 3, 5/) ! 追加するデータ
    call json%initialize()
    ! エラー処理
    if (json%failed()) then
        call json%print_error_message(io_unit)
    endif
    ! rootオブジェクトを作成
    call json%create_object(root, '')
    ! rootオブジェクトに整数を追加
    call json%add(root, 'int_scalar', int_scalar)
    call json%add(root, 'int_vector(odd)', int_vector) ! 配列も追加可能
    call json%add(root, 'int_vector(even)', [2_IK, 4_IK, 6_IK]) ! 直接与えることも可能
    call json%add(root, 'int_connection', [int_scalar, int_vector]) ! つなぐことも可能
    ! 作成したオブジェクトを標準出力
    call json%print(root, io_unit)
    ! 作成したオブジェクトをファイル出力
    call json%print(root, 'result.json')
    call json%destroy()
end program

結果として、以下の内容のJSONファイルの作成と標準出力が行われます。

{
  "int_scalar": 0,
  "int_vector(odd)": [
    1,
    3,
    5
  ],
  "int_vector(even)": [
    2,
    4,
    6
  ],
  "int_connection": [
    0,
    1,
    3,
    5
  ]
}

また整数以外のデータもJSONファイルに出力できます。例を以下に示します。

program main
    use json_module
    use json_module, IK => json_IK, &
                     RK => json_RK, &
                     CK => json_CK, &
                     LK => json_LK
    implicit none
    type(json_core) :: json
    type(json_value), pointer :: root ! rootオブジェクト用
    integer, parameter :: io_unit = 6 ! 標準出力に使用
    integer(IK) :: int_scalar = 5 ! 追加する整数データ
    character(kind=CK, len=4) :: char_data = "MOJI" ! 追加する文字列データ
    real(RK) :: real_scalar = 1.0d0 ! 追加する実数データ
    logical(LK) :: logical_data = .true. ! 追加する論理型データ
    call json%initialize()
    ! エラー処理
    if (json%failed()) then
        call json%print_error_message(io_unit)
    endif
    ! rootオブジェクトを作成
    call json%create_object(root, '')
    ! rootオブジェクトに整数を追加
    call json%add(root, 'int', int_scalar)
    call json%add(root, 'real', real_scalar)
    call json%add(root, 'char', char_data)
    call json%add(root, 'logical', logical_data)
    ! 作成したオブジェクトを標準出力
    call json%print(root, io_unit)
    ! 作成したオブジェクトをファイル出力
    call json%print(root, 'result.json')
    call json%destroy()
end program

結果として、以下の内容のJSONファイルの作成と標準出力が行われます。

{
  "int": 5,
  "real": 0.1E+1,
  "char": "MOJI",
  "logical": true
}

整数型の場合と同様に、配列やブラケット[]による出力も可能です。ただし、ブラケット[]で変数をつなぐ場合には、同じデータ型である必要があります。

オブジェクトのネスト

オブジェクトにはデータだけではなく、オブジェクト自体も追加できます。オブジェクトの作成と追加には、create_object()とadd()を使用します。また、親のオブジェクトを取得するにはget_parent()を使用します。get_parent()では第1引数に与えたオブジェクトの親へのポインタが、第2引数に代入されます。

program main
    use json_module
    use json_module, IK => json_IK
    implicit none
    type(json_core) :: json
    type(json_value), pointer :: root ! rootオブジェクト用のポインタ
    type(json_value), pointer :: inp
    integer, parameter :: io_unit = 6 ! 標準出力に使用

    call json%initialize()
    ! rootオブジェクトを作成
    call json%create_object(root, '')
    ! parametersオブジェクトを作成
    call json%create_object(inp, 'parameters')
    ! rootオブジェクトにparametersオプジェクトを追加
    call json%add(root, inp)
    ! parametersオブジェクトにデータを追加
    call json%add(inp, 'data', 3_IK)
    ! parametersオブジェクトの親をinpに代入
    call json%get_parent(inp, inp)
    ! parametersオブジェクトの親に文字列データを追加
    call json%add(inp, 'name', 'fogefoge')

    call json%print(root, io_unit)
    call json%print(root, 'result.json')
    call json%destroy()
end program    

このコードでは、まずrootオブジェクトにparametersという名前のオブジェクトを追加し、そこに整数型のデータを追加しました。そして、get_parent()によりparametersオブジェクトの親(root)を取得し、そこに文字列型のデータを追加しました。結果として、以下の内容のJSONファイルの作成と標準出力が行われます。

{
  "parameters": {
    "data": 3
  },
  "name": "fogefoge"
}

まとめ

FortranでJSONファイルに記述されたデータを読み書きする方法を示しました。

Fortranにはネームリストとよばれる独自のフォーマットのファイル読み書き方法がありますが、jsonのような入れ子の構造にできないため、データ間の関係が分からりずらく、エラー処理を正しくプログラミングしないと入力ミスが起こりやすい欠点があります。

それに対して、JSONは規格が厳密に決まっており、入れ子構造を採用していることからデータ間の関係もわかりやすいです。

よければ、試してみてください。

-TECHNOLOGY
-,