WebAPIを叩くと、レスポンスがJSON形式の文字列で返ってくることがあります。 ほとんどの場合そのまま文字列で扱うよりも、ライブラリを使ってプログラミング言語がサポートする辞書型に変換するのではないかと思います。
単純なJSONの場合は辞書型のままで操作・抽出してもよいかもしれませんが、 複雑なJSONの場合はごちゃごちゃと辞書を弄り回すよりも自分で定義した型にデータをマッピングしたいものです。
今回はJuliaにおけるJSON(から得られるDict
)のstruct
へのマッピング方法について書いてみたいと思います。
Parameters.jlを利用したマッピング
JuliaではネストしていないDict
であればマッピングは比較的容易です。例として次のようなJSONを考えます。
{
"title": "hello",
"name": "main_window",
"width": 500,
"height": 400,
"show": true
}
まずJSON.jl
で文字列からDict
に変換します。
julia> s = "{\r\n \"title\": \"hello\",\r\n \"name\": \"main_window\",\r\n \"width\": 500,\r\n \"height\": 400,\r\n \"show\": true\r\n}"
julia> println(s)
{
"title": "hello",
"name": "main_window",
"width": 500,
"height": 400,
"show": true
}
julia> using JSON
julia> j = JSON.parse(s)
Dict{String,Any} with 5 entries:
"name" => "main_window"
"height" => 400
"show" => true
"title" => "hello"
"width" => 500
ここまではいいと思います。
struct Window
name::String
title::String
show::Bool
height::UInt64
width::UInt64
end
上のようなstruct
にマッピングするのはParameters.jl
を付けば比較的容易です。
まずキーを文字列からシンボルに変換する次のような関数を作っておきます。
julia> keytosymbol(x) = Dict(Symbol(k) => v for (k, v) in pairs(x))
julia> keytosymbol(j)
Dict{Symbol,Any} with 5 entries:
:show => true
:name => "main_window"
:height => 400
:title => "hello"
:width => 500
Parameters.jlの@with_kw
マクロを使用すると
キーワードでコンストラクタが初期化できるようになります。(デフォルト値もサポートされています)
julia> @with_kw struct Window
name::String
title::String
show::Bool
height::UInt64
width::UInt64
end
Pythonでdict
型のオブジェクトをキーワード引数としてアンパックして関数に渡すことができる( f(**kwargs)
)ように、
Pythonでもキーワード引数にDict{Symbol, T}
型のオブジェクトをf(;kwargs...)
としてアンパックして渡すことができます。
以上のことから、先ほどの関数を使ってDict
のキーをSymbol
に変換し、splat
operator ...
でアンパックすれば、
julia> Window(;keytosymbol(j)...)
Window
name: String "main_window"
title: String "hello"
show: Bool true
height: UInt64 0x0000000000000190
width: UInt64 0x00000000000001f4
と無事にマッピングできました。
ネストされている場合
しかしながら、実際にレスポンスとして帰ってくるJSONはしばしばもっと複雑です。
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
上はJSON Exampleから引用したJSONの例です。
using Parameters
@with_kw struct GlossDef
para::String
glossseealso::Vector{String}
end
@with_kw struct GlossEntry
id::String
sortas::String
glossterm::String
acronym::String
abbrev::String
glossdef::GlossDef
glosssee::String
end
@with_kw struct GlossList
glossentry::GlossEntry
end
@with_kw struct GlossDiv
title::String
glosslist::GlossList
end
@with_kw struct Glossary
title::String
glossdiv::GlossDiv
end
上のように定義したstructにマッピングするにはどうしたらよいのでしょうか。
もちろん真面目に構成していけばできない事はないのですが、なるべくお手軽に変換したいものです。
julia> s = "{\r\n \"glossary\": {\r\n \"title\": \"example glossary\",\r\n\t\t\"GlossDiv\": {\r\n \"title\": \"S\",\r\n\t\t\t\"GlossList\": {\r\n \"GlossEntry\": {\r\n \"ID\": \"SGML\",\r\n\t\t\t\t\t\"SortAs\": \"SGML\",\r\n\t\t\t\t\t\"GlossTerm\": \"Standard Generalized Markup Language\",\r\n\t\t\t\t\t\"Acronym\": \"SGML\",\r\n\t\t\t\t\t\"Abbrev\": \"ISO 8879:1986\",\r\n\t\t\t\t\t\"GlossDef\": {\r\n \"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\r\n\t\t\t\t\t\t\"GlossSeeAlso\": [\"GML\", \"XML\"]\r\n },\r\n\t\t\t\t\t\"GlossSee\": \"markup\"\r\n }\r\n }\r\n }\r\n }\r\n}"
"{\r\n \"glossary\": {\r\n \"title\": \"example glossary\",\r\n\t\t\"GlossDiv\": {\r\n \"title\": \"S\",\r\n\t\t\t\"GlossList\": {\r\n \"GlossEntry\": {\r\n \"ID\": \"SGML\",\r\n\t\t\t\t\t\"SortAs\": \"SGML\",\r\n\t\t\t\t\t\"GlossTerm\": \"Standard Generalized Markup Language\",\r\n\t\t\t\t\t\"Acronym\": \"SGML\",\r\n\t\t\t\t\t\"Abbrev\": \"ISO 8879:1986\",\r\n\t\t\t\t\t\"GlossDef\": {\r\n \"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\r\n\t\t\t\t\t\t\"GlossSeeAlso\": [\"GML\", \"XML\"]\r\n },\r\n\t\t\t\t\t\"GlossSee\": \"markup\"\r\n }\r\n }\r\n }\r\n }\r\n}"
julia> j = JSON.parse(s)["glossary"]
Dict{String,Any} with 2 entries:
"title" => "example glossary"
"GlossDiv" => Dict{String,Any}("title"=>"S","GlossList"=>Dict{String,Any}("GlossEntry"=>Dict{String,Any}("GlossSee"…
キーを小文字に変換
まずJSONのキーを小文字にしましょう。再帰的に書くと次のようになります。
julia> lowercasekeys(x) = x
julia> lowercasekeys(x::AbstractDict) = Dict(lowercase(k) => lowercasekeys(v) for (k, v) in pairs(x))
julia> j2 = lowercasekeys(j)
Dict{String,Any} with 2 entries:
"glossdiv" => Dict{String,Any}("title"=>"S","glosslist"=>Dict{String,Dict{String,Any}}("glossentry"=>Dict("sortas"=>"SGML","abbrev"=>"ISO 8879:1986","id"…
"title" => "example glossary"
以前と同じような方法では、Dict
をGlossDiv
にconvert
できないので失敗します。
julia> Glossary(;keytosymbol(j2)...)
ERROR: MethodError: Cannot `convert` an object of type Dict{String,Any} to an object of type GlossDiv
Closest candidates are:
convert(::Type{T}, ::T) where T at essentials.jl:171
GlossDiv(::Any, ::Any) at /root/.julia/packages/Parameters/CVyBv/src/Parameters.jl:480
Stacktrace:
[1] Glossary(::String, ::Dict{String,Any}) at /root/.julia/packages/Parameters/CVyBv/src/Parameters.jl:480
[2] Glossary(; title::String, glossdiv::Dict{String,Any}) at /root/.julia/packages/Parameters/CVyBv/src/Parameters.jl:468
[3] top-level scope at REPL[31]:1
関数の形を考えてみる
変換する関数は大まかにこんな感じになるはずです。(適当)
function convertdict(T::Type, d::AbstractDict)
kwargs = Dict{Symbol, Any}()
for (k, v) in pairs(d)
symk = Symbol(k)
kwargs[symk] = (変換が必要な時は変換する)
end
return T(;kwargs...)
end
「変換が必要な時は変換する」という部分を考えましょう。
変換先の型を判別
変換がいつ必要になるかはメンバー変数の型を見て判別することにします。
ちなみに、メンバー変数の型はCore.fieldtype
で取得できます。
julia> fieldtype(GlossDef, :para)
String
julia> fieldtype(GlossDef, :glossseealso)
Array{String,1}
Column: fieldtype(T, k)
の型, Row: v
の型 としたとき、下のようにディスパッチさせることにします。
Union{S, Nothing} |
Vector{S} |
Dict |
それ以外 | |
---|---|---|---|---|
Vector |
convertdict(S, v) |
convertdict.(S, v) |
— | — |
Dict |
convertdict(S, v) |
— | v |
マッピングを実施 |
それ以外 | convertdict(S, v) |
— | — | v |
convertdict
の構成
あとは先ほどの表に合わせて関数を整えていきます。
convertdict(::Type, x) = x
convertdict(::Type{Union{T, Nothing}}, x) where T = convertdict(T, x)
convertdict(::Type{Union{T, Nothing}}, d::AbstractDict) where T = convertdict(T, d)
convertdict(::Type{T}, v::AbstractVector) where T <: AbstractVector = convertdict.(eltype(T), v)
convertdict(::Type{T}, d::AbstractDict) where T <: AbstractDict = d
function convertdict(T::Type, d::AbstractDict)
kwargs = Dict{Symbol, Any}()
for (k, v) in pairs(d)
symk = Symbol(k)
kwargs[symk] = convertdict(fieldtype(T, symk), v)
end
return T(;kwargs...)
end
思ったよりシンプルですね。3行目の
convertdict(::Type{Union{T, Nothing}}, d::AbstractDict) where T = convertdict(T, d)
は2行目に含まれているので必要ないように見えるかもしれませんが、これがないと
convertdict(::Type{Union{T, Nothing}}, d::AbstractDict)
が
convertdict(::Type{Union{T, Nothing}}, x)
と
convertdict(T::Type, d::AbstractDict)
のどちらにディスパッチしてよいか曖昧になってしまうので、必要な定義です。
さてさて、
julia> glossary = convertdict(Glossary, j2)
Glossary
title: String "example glossary"
glossdiv: GlossDiv
julia> glossary.glossdiv
GlossDiv
title: String "S"
glosslist: GlossList
julia> glossary.glossdiv.glosslist
GlossList
glossentry: GlossEntry
julia> glossary.glossdiv.glosslist.glossentry
GlossEntry
id: String "SGML"
sortas: String "SGML"
glossterm: String "Standard Generalized Markup Language"
acronym: String "SGML"
abbrev: String "ISO 8879:1986"
glossdef: GlossDef
glosssee: String "markup"
julia> glossary.glossdiv.glosslist.glossentry.glossdef
GlossDef
para: String "A meta-markup language, used to create markup languages such as DocBook."
glossseealso: Array{String}((2,))
julia> glossary.glossdiv.glosslist.glossentry.glossdef.glossseealso
2-element Array{String,1}:
"GML"
"XML"
一発でマッピングできました。
もう一例やってみます。
{"menu": {
"header": "SVG Viewer",
"items": [
{"id": "Open"},
{"id": "OpenNew", "label": "Open New"},
null,
{"id": "ZoomIn", "label": "Zoom In"},
{"id": "ZoomOut", "label": "Zoom Out"},
{"id": "OriginalView", "label": "Original View"},
null,
{"id": "Quality"},
{"id": "Pause"},
{"id": "Mute"},
null,
{"id": "Find", "label": "Find..."},
{"id": "FindAgain", "label": "Find Again"},
{"id": "Copy"},
{"id": "CopyAgain", "label": "Copy Again"},
{"id": "CopySVG", "label": "Copy SVG"},
{"id": "ViewSVG", "label": "View SVG"},
{"id": "ViewSource", "label": "View Source"},
{"id": "SaveAs", "label": "Save As"},
null,
{"id": "Help"},
{"id": "About", "label": "About Adobe CVG Viewer..."}
]
}}
まずは読み込んでみます。
julia> s2 = "{\"menu\": {\r\n \"header\": \"SVG Viewer\",\r\n \"items\": [\r\n {\"id\": \"Open\"},\r\n {\"id\": \"OpenNew\", \"label\": \"Open New\"},\r\n null,\r\n {\"id\": \"ZoomIn\", \"label\": \"Zoom In\"},\r\n {\"id\": \"ZoomOut\", \"label\": \"Zoom Out\"},\r\n {\"id\": \"OriginalView\", \"label\": \"Original View\"},\r\n null,\r\n {\"id\": \"Quality\"},\r\n {\"id\": \"Pause\"},\r\n {\"id\": \"Mute\"},\r\n null,\r\n {\"id\": \"Find\", \"label\": \"Find...\"},\r\n {\"id\": \"FindAgain\", \"label\": \"Find Again\"},\r\n {\"id\": \"Copy\"},\r\n {\"id\": \"CopyAgain\", \"label\": \"Copy Again\"},\r\n {\"id\": \"CopySVG\", \"label\": \"Copy SVG\"},\r\n {\"id\": \"ViewSVG\", \"label\": \"View SVG\"},\r\n {\"id\": \"ViewSource\", \"label\": \"View Source\"},\r\n {\"id\": \"SaveAs\", \"label\": \"Save As\"},\r\n null,\r\n {\"id\": \"Help\"},\r\n {\"id\": \"About\", \"label\": \"About Adobe CVG Viewer...\"}\r\n ]\r\n}}"
julia> println(s2)
{"menu": {
"header": "SVG Viewer",
"items": [
{"id": "Open"},
{"id": "OpenNew", "label": "Open New"},
null,
{"id": "ZoomIn", "label": "Zoom In"},
{"id": "ZoomOut", "label": "Zoom Out"},
{"id": "OriginalView", "label": "Original View"},
null,
{"id": "Quality"},
{"id": "Pause"},
{"id": "Mute"},
null,
{"id": "Find", "label": "Find..."},
{"id": "FindAgain", "label": "Find Again"},
{"id": "Copy"},
{"id": "CopyAgain", "label": "Copy Again"},
{"id": "CopySVG", "label": "Copy SVG"},
{"id": "ViewSVG", "label": "View SVG"},
{"id": "ViewSource", "label": "View Source"},
{"id": "SaveAs", "label": "Save As"},
null,
{"id": "Help"},
{"id": "About", "label": "About Adobe CVG Viewer..."}
]
}}
julia> j3 = JSON.parse(s2)["menu"]
Dict{String,Any} with 2 entries:
"items" => Any[Dict{String,Any}("id"=>"Open"), Dict{String,Any}("label"=>"Open New","id"=>"OpenNew"), nothing, Dict{String,Any}("label"=>"Zoom In","id"=…
"header" => "SVG Viewer"
null
はnothing
にマッピングされています。
julia> @with_kw struct Item
id::String
label::Union{String, Nothing} = nothing
end
julia> @with_kw struct Menu
header::String
items::Vector{Union{Item, Nothing}}
end
上のように定義した場合でも、
julia> menu = convertdict(Menu, j3)
Menu
header: String "SVG Viewer"
items: Array{Union{Nothing, Item}}((22,))
julia> menu.items
22-element Array{Union{Nothing, Item},1}:
Item
id: String "Open"
label: Nothing nothing
Item
id: String "OpenNew"
label: String "Open New"
nothing
Item
id: String "ZoomIn"
label: String "Zoom In"
Item
id: String "ZoomOut"
label: String "Zoom Out"
Item
id: String "OriginalView"
label: String "Original View"
nothing
Item
id: String "Quality"
label: Nothing nothing
Item
id: String "Pause"
label: Nothing nothing
Item
id: String "Mute"
label: Nothing nothing
⋮
Item
id: String "Copy"
label: Nothing nothing
Item
id: String "CopyAgain"
label: String "Copy Again"
Item
id: String "CopySVG"
label: String "Copy SVG"
Item
id: String "ViewSVG"
label: String "View SVG"
Item
id: String "ViewSource"
label: String "View Source"
Item
id: String "SaveAs"
label: String "Save As"
nothing
Item
id: String "Help"
label: Nothing nothing
Item
id: String "About"
label: String "About Adobe CVG Viewer..."
問題なくnothing
やVector
とUnion
の組み合わせを処理できています。
パッケージ
パッケージとするほどのものでもないかもしれませんが、一応JuliaRegistries/Generalに登録してあります。
] add StructMapping
でお使いください。