JSONデータをDataTableに格納したい

タグの編集
投稿者 まほ  (学生) 投稿日時 2021/4/15 19:57:01
VB初心者です。
ファイル化されたJSONデータを読込みDataTableに格納しようとしているのですが思ったように格納できません。

読み込むJSONは以下の形式です。
「list」と「grouplist」のキー名は決まっています。
ただしgrouplistの要素数とそれに対応するグループのキー及び要素数は読込むJSONごとに異なります。

-------------------------------------------------------------------------
例:Json1
{
"list": {
"grouplist": ["fruit", "animal", "flower"],
"fruit": ["リンゴ","バナナ","イチゴ"],
"animal": ["イヌ","ネコ","ウサギ"],
"flower": ["バラ","ユリ","キク"]
}
}
-------------------------------------------------------------------------
例:Json2
{
"list": {
"grouplist": ["fruit","animal","flower","human"],
"fruit": ["リンゴ","バナナ","イチゴ","モモ","ブドウ"],
"animal": ["イヌ","ネコ","ウサギ","リス","ウシ"],
"flower": ["バラ","ユリ","キク","チューリップ","ヒマワリ"]
"human": ["タナカ","サトウ","ヤマダ","カトウ"],
}
}
-------------------------------------------------------------------------
   Imports System.IO
   Imports System.Text
   Imports Newtonsoft.Json
   Imports Newtonsoft.Json.Linq

   Public JsonDataTable As New DataTable


   Private Sub AppLoad(sender As Object, e As EventArgs) Handles Me.Load
        Call LoadJson(Application.StartupPath & "\test.json")
   End Sub

   Private Sub LoadJson(ByVal FilePath As String)

        Dim enc As Encoding = Encoding.UTF8
        Dim jsonStr As String

        'ファイルからJson文字列を読み込む 
        Using sr As New StreamReader(FilePath, enc)
            jsonStr = sr.ReadToEnd()
        End Using

        'Json文字列をJson形式データに復元する 
        Dim jsonObj As Object = JsonConvert.DeserializeObject(jsonStr)

        'listからgrouplist内の要素を取得 
        Dim GrouplistArray As JArray = CType(jsonObj("list")("grouplist"), JArray)

        'listのgrouplist内の要素を全て取得 
        For i As Integer = 0 To GrouplistArray.Count - 1

            'グループの列を作成 
            Dim CurrentGroupName As String = GrouplistArray(i).ToString
            JsonDataTable.Columns.Add(CurrentGroupName)

            'グループの要素を配列に格納 
            Dim GroupItemArray As JArray = CType(jsonObj("list")(CurrentGroupName), JArray)

            'グループの配列要素をDataTableに格納 
            For Each Item As String In GroupItemArray
                Dim row As DataRow = JsonDataTable.NewRow
                row(CurrentGroupName) = Item
                JsonDataTable.Rows.Add(row)
            Next

        Next

        'DataTableの内容をDataGridViewに表示 
        DataGridView1.DataSource = JsonDataTable

    End Sub

このように記述すると二つ目以降のグループ要素が一つ前のグループ要素の次の行から格納されてずれてしまいます。

| fruit  | animal  | flower |
|リンゴ |           |          |
|バナナ|           |          |
|イチゴ |           |          |
|         |イヌ      |          |
|         |ネコ      |          |
|         |ウサギ  |          |
|        |          |バラ    |
|        |          |ユリ    |
|        |          |キク    |


読み込むJSONによってグループ数が不確定なので、列ごとにループで要素を格納するしか思いつかずこのような形になってしまいます。
全てのグループの要素を一行目からセットするにはどのように記述すればよいでしょうか?
ご教授をお願いします。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2021/4/16 10:34:47
> 読み込むJSONは以下の形式です。
Json2 のカンマの位置がおかしいので、構文エラーになりそうです。

> ただしgrouplistの要素数とそれに対応するグループのキー及び要素数は読込むJSONごとに異なります。
たとえば、animal が 3 要素で、flower が 7 要素といった非対称性もありえるのですね。
(実際、Json2 の human は要素数が他より少ない)

> 全てのグループの要素を一行目からセットするにはどのように記述すればよいでしょうか?
やり方は色々あると思いますが、例えばこんな感じにしてみるとか。

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim jsonObj = JsonConvert.DeserializeObject(Of JObject)(TextBox1.Text)
    Dim jsonDataTable As New DataTable("JsonDataTable")
    For Each grp In jsonObj("list")("grouplist")
        Dim key = grp.ToString()
        Dim tbl As New DataTable()
        tbl.Columns.Add("ID"GetType(Long)).AutoIncrement = True
        tbl.Columns.Add(key)
        tbl.PrimaryKey = New DataColumn() {tbl.Columns(0)}
        For Each row In jsonObj("list")(key)
            Dim newRow = tbl.NewRow()
            newRow.SetField(Of String)(key, row.ToString())
            tbl.Rows.Add(newRow)
        Next
        jsonDataTable.Merge(tbl, False, MissingSchemaAction.AddWithKey)
    Next
    jsonDataTable.PrimaryKey = Nothing
    jsonDataTable.Columns.RemoveAt(0)
    jsonDataTable.AcceptChanges()
    DataGridView1.DataSource = jsonDataTable
End Sub
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2021/4/16 13:44:02
> やり方は色々あると思いますが、例えばこんな感じにしてみるとか。

別例

Dim jsonList = JsonConvert.DeserializeObject(Of JObject)(TextBox1.Text)("list")
Dim cols = jsonList("grouplist").Select(Function(g) g.ToString())
Dim jsonDataTable As New DataTable("JsonDataTable")
jsonDataTable.Columns.AddRange(cols.Select(Function(c) New DataColumn(c)).ToArray())
For rowIndex = 0 To Integer.MaxValue - 1
    Dim newRow = jsonDataTable.NewRow()
    For Each col In cols
        newRow(col) = jsonList(col).ElementAtOrDefault(rowIndex)
    Next
    If newRow.ItemArray.All(AddressOf IsDBNull) Then
        Exit For
    End If
    jsonDataTable.Rows.Add(newRow)
Next
jsonDataTable.AcceptChanges()
DataGridView1.DataSource = jsonDataTable
投稿者 まほ  (学生) 投稿日時 2021/4/16 19:38:32
魔界の仮面弁士様

お返事ありがとうございます。
すみません、Jsonはサンプルに手打ちで記述したものでカンマ位置を間違えていました。
はい、fruit等のキー名やキー数、その中の要素名や要素数もJSON個々に違います。

ご提示頂いたプログラムを試させて頂いたところ各行の一行目から正しく要素がセットされ思い通りの結果になりました。本当にありがとうございます。
二つ目の例はコードがすっきりしているのでこちらを使用させて頂きたいと思います。

ただ両方のコードを見てもどのような事をしているのか理解出来なかったので、よろしければ今後の勉強の為にこういった場合の考え方(組み方)のご解説を頂けると大変助かります。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2021/4/16 20:56:30
まずは最初の例から。

> Dim tbl As New DataTable()
grouplist を列挙して、fruit, aminal, flower 用に、それぞれの別の DataTable を作成しています。
このループ処理により、下記の 3 つのテーブルが生成されます。

   🍎🍌🍓   🐶😺🐰   🌹💐🌼
┏━┯━━━┓┏━┯━━━┓┏━┯━━━┓
┃ID│fruit ┃┃ID│animal┃┃ID│flower┃
┣━┿━━━┫┣━┿━━━┫┣━┿━━━┫
┃ 0│リンゴ┃┃ 0│イヌ  ┃┃ 0│バラ  ┃
┃ 1│バナナ┃┃ 1│ネコ  ┃┃ 1│ユリ  ┃
┃ 2│イチゴ┃┃ 2│ウサギ┃┃ 2│キク  ┃
┗━┷━━━┛┗━┷━━━┛┗━┷━━━┛

『ID』列が追加されていますが、これはテーブルの各行を識別するための「主キー」です。
これは、下記のコードで生成されています。

> tbl.Columns.Add("ID", GetType(Long)).AutoIncrement = True
> tbl.PrimaryKey = New DataColumn() {tbl.Columns(0)}

ID 列の値は自分で代入していっても良いのですが、ここでは AutoIncrement を
用いてます。これを使うと、新しい行が追加されるたびに自動採番してくれます。


> jsonDataTable.Merge(tbl, False, MissingSchemaAction.AddWithKey)
作成されたテーブルは、jsonDataTable にマージしていき、下記の表を作ります。
Merge メソッドに指定してある AddWithKey が肝となっており、これによって
主キーが一致するデータを同一行とみなして結合してくれます。

┏━┯━━━┯━━━┯━━━┓
┃ID│fruit │animal│flower┃
┣━┿━━━┿━━━┿━━━┫
┃ 0│リンゴ│イヌ  │バラ  ┃
┃ 1│バナナ│ネコ  │ユリ  ┃
┃ 2│イチゴ│ウサギ│キク  ┃
┗━┷━━━┷━━━┷━━━┛



> jsonDataTable.PrimaryKey = Nothing
> jsonDataTable.Columns.RemoveAt(0)
最終結果に ID 列は不要なので、最後に取り除きます。
削除前には、PrimaryKey としての割り当てを解除しておく必要があります。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2021/4/16 21:32:14
2 つ目のコードについても解説。


> Dim jsonList = JsonConvert.DeserializeObject(Of JObject)(TextBox1.Text)("list")
ここでは、(Of JObject) の型パラメーター付きのオーバーロードを用いています。
型パラメーターを使わず、Object で受け取ってから、CType や DirectCast を使っても良いです。
Dim jsonList = DirectCast(JsonConvert.DeserializeObject(TextBox1.Text), JObject)("list")


> Dim cols = jsonList("grouplist").Select(Function(g) g.ToString())
LINQ を用いて、grouplist 内の値を列挙しています。
上記の実行結果は、下記のようになります。
Dim cols() As String = {"fruit""animal""flower"}


> jsonDataTable.Columns.AddRange(cols.Select(Function(c) New DataColumn(c)).ToArray())
ここでは LINQ を用いて、3 つの列を一度に追加していますが、
下記のように、ループ処理で 3 回に分けて追加しても構いません。
For Each c As String In cols
   jsonDataTable.Columns.Add(New DataColumn(c))
Next



> For rowIndex = 0 To Integer.MaxValue - 1
To に渡した Integer.MaxValue は 2147483647 と同義ですので、
それを -1 して、rowIndex が 0~2147483646 の間で遷移していきます。

わざわざ上限値を 1 つ減らしているのは、OverflowException の発生を避けるための処置です。
Next に到達する度に、rowIndex が +1 されるためですね。

もしも上限いっぱいの 0~2147483647 の間で遷移させたい場合には、
「For rowIndex As Long = 0L To Integer.MaxValue」
のように、ループカウンターを Long 型にすることができます。

しかし、この後で呼び出す ElementAtOrDefault メソッドは Integer 型を必要とするため、
今回は As Long とはせず、Integer 型のままにしてあります。


> Dim newRow = jsonDataTable.NewRow()
> For Each col In cols
>     newRow(col) = jsonList(col).ElementAtOrDefault(rowIndex)
> Next

ElementAtDefault は、該当する位置の値が無い場合に Nothing を返します。

jsonList(col)(rowIndex) だと、インデックスが範囲外エラーになる可能性があるので、ここでは
jsonList(col).ElementAtOrDefault(rowIndex) をかわりに利用しています。


このループにより、下記のような動作が行われます。

newRow = jsonDataTable.NewRow()   '新しい行を作る 
newRow("fruit" ) = jsonList("fruit" ).ElementAtOrDefault(0)  'リンゴ 
newRow("animal") = jsonList("animal").ElementAtOrDefault(0)  'イヌ 
newRow("flower") = jsonList("flower").ElementAtOrDefault(0)  'バラ 
ここまで作ってから、DataTable に一行追加。

newRow = jsonDataTable.NewRow()   '新しい行を作る 
newRow("fruit" ) = jsonList("fruit" ).ElementAtOrDefault(1)  'バナナ 
newRow("animal") = jsonList("animal").ElementAtOrDefault(1)  'ネコ 
newRow("flower") = jsonList("flower").ElementAtOrDefault(1)  'ユリ 
ここまで作ってから、DataTable にまた一行追加。


>  If newRow.ItemArray.All(AddressOf IsDBNull) Then
>      Exit For
>  End If
fruit、animal、flower の 3 つすべてが Nothing だった場合は、
もうデータが無いことになるので、行追加のループを脱出させます。

JSON データを直接確認するなら、Nothing 判定のため IsNothing を使うところですが、
DataTable に格納したデータは DBNull に変化するため、IsNull で判定しています。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2021/4/16 21:38:23
ちょっと訂正。

>> Dim cols = jsonList("grouplist").Select(Function(g) g.ToString())
> 上記の実行結果は、下記のようになります。
> Dim cols() As String = {"fruit", "animal", "flower"}

上記の説明は正しくありませんね。

「Dim cols() As String」にするためのコードはこうです。

Dim cols = jsonList("grouplist").Select(Function(g) g.ToString()).ToArray()


今回は .ToArray() していなかったので、実際は 「Dim cols As IEnumerable(Of String)」になっています。
投稿者 まほ  (学生) 投稿日時 2021/4/17 19:56:58
ご丁寧なご解説をありがとうございます。
なるほど、一つ目の例はそれぞれのグループを別々のテーブルに格納して最後に一つのDataTableに結合するのですね。
二つ目の例は勉強不足で完全に理解するのにまだ時間が掛かりそうですが、柔軟に組めるよう今後も勉強して参りたいと思います。