Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JSON Schema で複雑な仕様の入力フォームの実装に立ち向かった話

solt9029
October 22, 2021

JSON Schema で複雑な仕様の入力フォームの実装に立ち向かった話

Ruby on Rails を用いたシステム上で入力フォームを実現する際、Rails が提供しているフォームヘルパーを利用した実装や、React や Vue によるコンポーネントの自前での実装が一般的に行われます。

ここで、職業で学生を選択した場合は学校名と学年、会社員を選択した場合は役職と年収を入力する...といった、条件分岐が大量に生まれる入力フォームを想像しましょう。
一般的な実装手法では、あるフォームの入力値が他のフォームに影響を与えるような、複雑で動的な入力フォームの実現をするために、大量の if 文を書く必要があります。
また、ユーザから送信された入力値の正しさをバリデーションするために、バックエンド側に同様の if 文を大量に書く必要が出てきます。

そこで私は、複雑な仕様の入力フォームの実装のための JSON Schema 活用方法および事例について紹介します。入力フォームの仕様を JSON Schema で定義すれば、大量の if 文を用いることなくフロントエンド側の入力フォームを自動生成することができます。また、その JSON Schema をバックエンド側のバリデーションのロジックにも使い回すことができるため、シンプルでメンテナンス性の高い実装を実現することができます。

solt9029

October 22, 2021
Tweet

More Decks by solt9029

Other Decks in Programming

Transcript

  1. +40/4DIFNBͰ
    ෳࡶͳ࢓༷ͷೖྗϑΥʔϜͷ࣮૷ʹ
    ཱͪ޲͔ͬͨ࿩
    ΫοΫύουגࣜձࣾ Ԙग़ ݚ࢙
    1

    View Slide

  2. 2
    Ԙग़ ݚ࢙
    ৯Ԙग़ݱ !TPMU
    ΫοΫύουגࣜձࣾ ങ෺ϓϩμΫτ։ൃ෦ ॴଐ
    ීஈ͸ 3BJMT΍ 3FBDUΛॻ͘
    झຯ͸ϓϩτλΠϐϯάʢ࠷ۙ͸͓ֆඳ͖΋޷͖ʣ
    ҿञ TVEPΛ๷ࢭ͢ΔγεςϜ

    View Slide

  3. 3
    +40/4DIFNBΛ࢖࣮ͬͯݱͨ͠΋ͷ

    View Slide

  4. 4
    ࢲ͕։ൃʹܞΘ͍ͬͯΔϓϩμΫτ
    ੜ઱৯඼Λத৺ͱͨ͠&$ϓϥοτϑΥʔϜ

    View Slide

  5. 5
    ൢചऀ޲͚؅ཧը໘
    • ঎඼৘ใͷొ࿥
    • Ӧۀ೔ͷ؅ཧ
    • ग़ՙ࡞ۀͷ֬ೝ
    • ঎඼ϨϏϡʔͷฦ৴

    View Slide

  6. 6
    ൢചऀ޲͚؅ཧը໘
    • ঎඼৘ใͷొ࿥
    • Ӧۀ೔ͷ؅ཧ
    • ग़ՙ࡞ۀͷ֬ೝ
    • ঎඼ϨϏϡʔͷฦ৴

    View Slide

  7. 7
    ঎඼৘ใͷొ࿥
    • ͓ૣΊʹ͓ঌ্͕͠Γͩ͘
    ͍͞
    • ফඅظݶ·Ͱͷ೔਺
    • ղౚ඼͔Ͳ͏͔
    • ੜ৯༻͔Ճ೤༻͔
    • ཆ৩͔ఱવ͔
    • ফඅظݶ·Ͱͷ೔਺
    • ղౚ඼͔Ͳ͏͔
    ೖྗ͢΂͖඼࣭อূ΍৯඼දࣔʹؔΘΔ৘ใ͸঎඼ͷछྨ͝ͱʹҟͳΔ

    View Slide

  8. 8
    ౰࣌ɺશ߲໨Λཏྻ͢ΔΑ͏ͳϑΥʔϜΛఏڙ͍ͯͨ͠😨
    • ೖྗϛε͕සൃ͠ɺ঎඼ొ࿥ʹख͕͔͔ؒΔ
    • ೑ͳͷʹʮ͓ૣΊʹ͓ঌ্͕͠Γ͍ͩ͘͞ʯΛબ୒ͯ͠͠·͏
    • ڕͳͷʹཆ৩͔ఱવ͔ೖྗ͠๨Εͯ͠·͏
    ϑϩϯτΤϯυଆͰ঎඼ͷछྨʹ΋ͱͮ͘ϑΥʔϜͷग़͠෼͚
    • ࣾ಺ͷӡ༻ϝϯόʹΑΔ঎඼৹ࠪͷෛ୲͕૿͑Δ
    όοΫΤϯυଆͰෆਖ਼ͳσʔλΛআͨ͘ΊͷόϦσʔγϣϯ

    View Slide

  9. 9
    • ೖྗϛε͕සൃ͠ɺ঎඼ొ࿥ʹख͕͔͔ؒΔ
    • ೑ͳͷʹʮ͓ૣΊʹ͓ঌ্͕͠Γ͍ͩ͘͞ʯΛબ୒ͯ͠͠·͏
    • ڕͳͷʹཆ৩͔ఱવ͔ೖྗ͠๨Εͯ͠·͏
    ϑϩϯτΤϯυଆͰ঎඼ͷछྨʹ΋ͱͮ͘ϑΥʔϜͷग़͠෼͚
    • ࣾ಺ͷӡ༻ϝϯόʹΑΔ঎඼৹ࠪͷෛ୲͕૿͑Δ
    όοΫΤϯυଆͰෆਖ਼ͳσʔλΛআͨ͘ΊͷόϦσʔγϣϯ
    ౰࣌ɺશ߲໨Λཏྻ͢ΔΑ͏ͳϑΥʔϜΛఏڙ͍ͯͨ͠😨

    View Slide

  10. 10
    Ͳ͏΍࣮ͬͯ૷͢Δʁ
    def validate!
    if category == "魚"
    if quality_guarantee_type != "消費期限"
    raise Error.new("魚なのに消費期限が…")
    end
    if farmed == nil
    raise Error.new("魚なのに養殖かどうか…")
    end
    # if ...
    elsif category == "肉"
    # if ...
    elsif category == "根もの"
    # if ...
    end
    end
    • ࠓޙɺ঎඼ͷछྨΛ௥Ճͨ͘͠ͳͬͨͱ͖
    ʹϝϯςφϯε͕େม
    • ϑϩϯτΤϯυͱόοΫΤϯυͰಉ༷ͷ
    JGจΛେྔʹॻ͘ඞཁ͕͋Δ
    • ͳΜ͔Πέͯͳͦ͞͏ʂ
    👎 ՝୊
    JG จΛେྔʹ࢖ͬͯΈΔ

    View Slide

  11. 11
    Ͳ͏΍࣮ͬͯ૷͢Δʁ
    +40/4DIFNBΛར༻͢Δ
    {
    required: ["raw", "thawed", "farmed"],
    properties: {
    category_id: { const: "魚のID" },
    thawed: { "$ref" => "#/definitions/thawed" },
    farmed: { "$ref" => "#/definitions/farmed" },
    raw: { "$ref" => "#/definitions/raw" },
    },
    dependencies: {
    thawed: {
    oneOf: [
    # 中略
    ],
    },
    },
    }
    • ࠓޙɺ঎඼ͷछྨΛ௥Ճͨ͘͠ͳͬͨͱ͖
    ͷϝϯςφϯεੑ͕ߴ͍
    • ϑϩϯτΤϯυͱόοΫΤϯυͷϩδοΫ
    ͕ڞ௨ͷεΩʔϚʹΑΓ׬੒͢Δ
    👍 ϝϦοτ

    View Slide

  12. 12
    +40/4DIFNBͱ͸
    {
    id: 100,
    name: "カルボナーラの作り方",
    content: "ベーコンと玉ねぎを食べやすい大き
    さに切ります。〜(以下略)",
    public: true
    }
    +40/4DIFNBͱ͸ +40/ͷߏ଄Λఆٛͨ͠΋ͷ
    ྉཧϨγϐͷ +40/ͷߏ଄Λఆٛͯ͠ΈΑ͏
    +40/ͷྫ

    View Slide

  13. 13
    +40/4DIFNBͱ͸
    {
    id: 100,
    name: "カルボナーラの作り方",
    content: "ベーコンと玉ねぎを食べやすい大き
    さに切ります。〜(以下略)",
    public: true
    }
    +40/4DIFNBͱ͸ +40/ͷߏ଄Λఆٛͨ͠΋ͷ
    ྉཧϨγϐͷ +40/ͷߏ଄Λఆٛͯ͠ΈΑ͏
    {
    title: "お料理レシピ",
    type: "object",
    properties: {
    id: { title: "ID", type: "integer" },
    name: { title: "名前", type: "string" },
    content: { title: "作り方", type: "string" },
    public: { title: "公開中", type: "boolean" }
    },
    required: ["id", "name", "content", "public"]
    }
    +40/ͷྫ
    +40/4DIFNBͷྫ

    View Slide

  14. 14
    +40/4DIFNBͱ͸
    {
    id: 100,
    name: "カルボナーラの作り方",
    content: "ベーコンと玉ねぎを食べやすい大き
    さに切ります。〜(以下略)",
    public: true
    }
    +40/4DIFNBͱ͸ +40/ͷߏ଄Λఆٛͨ͠΋ͷ
    ྉཧϨγϐͷ +40/ͷߏ଄Λఆٛͯ͠ΈΑ͏
    {
    title: "お料理レシピ",
    type: "object",
    properties: {
    id: { title: "ID", type: "integer" },
    name: { title: "名前", type: "string" },
    content: { title: "作り方", type: "string" },
    public: { title: "公開中", type: "boolean" }
    },
    required: ["id", "name", "content", "public"]
    }
    +40/ͷྫ
    +40/4DIFNBͷྫ
    ܕͳͲ੍໿͕ఆٛͰ͖Δ

    View Slide

  15. 15
    ඼࣭อূͷೖྗϑΥʔϜΛ +40/4DIFNBͰॻ͍ͯΈΔ
    {
    title: "商品",
    type: "object",
    required: ["category_id"],
    properties: {
    category_id: { title: "カテゴリ", type: "number", enum: [1, 2, 3], enumNames: ["根もの", "鶏肉", "魚介加工品"] }
    },
    dependencies: {
    category_id: {
    oneOf: [
    { properties: { category_id: { const: 1 }, /** 根もの独自の入力項目 **/ }},
    { properties: { category_id: { const: 2 }, /** 鶏肉独自の入力項目 **/ }},
    { properties: { category_id: { const: 3 }, /** 魚介加工品独自の入力項目 **/ }},
    ]
    },
    },
    }

    View Slide

  16. 16
    {
    title: "商品",
    type: "object",
    required: ["category_id"],
    properties: {
    category_id: { title: "カテゴリ", type: "number", enum: [1, 2, 3], enumNames: ["根もの", "鶏肉", "魚介加工品"] }
    },
    dependencies: {
    category_id: {
    oneOf: [
    { properties: { category_id: { const: 1 }, /** 根もの独自の入力項目 **/ }},
    { properties: { category_id: { const: 2 }, /** 鶏肉独自の入力項目 **/ }},
    { properties: { category_id: { const: 3 }, /** 魚介加工品独自の入力項目 **/ }},
    ]
    },
    },
    }
    EFQFOEFODJFTΧςΰϦ*%͕ઃఆ͞Ε͍ͯΔͱ͖ʜ
    ඼࣭อূͷೖྗϑΥʔϜΛ +40/4DIFNBͰॻ͍ͯΈΔ

    View Slide

  17. 17
    {
    title: "商品",
    type: "object",
    required: ["category_id"],
    properties: {
    category_id: { title: "カテゴリ", type: "number", enum: [1, 2, 3], enumNames: ["根もの", "鶏肉", "魚介加工品"] }
    },
    dependencies: {
    category_id: {
    oneOf: [
    { properties: { category_id: { const: 1 }, /** 根もの独自の入力項目 **/ }},
    { properties: { category_id: { const: 2 }, /** 鶏肉独自の入力項目 **/ }},
    { properties: { category_id: { const: 3 }, /** 魚介加工品独自の入力項目 **/ }},
    ]
    },
    },
    }
    EFQFOEFODJFTΧςΰϦ*%͕ઃఆ͞Ε͍ͯΔͱ͖ʜ
    POF0GͲΕ͔ʹҰக͢Δඞཁ͕͋Δ
    ඼࣭อূͷೖྗϑΥʔϜΛ +40/4DIFNBͰॻ͍ͯΈΔ

    View Slide

  18. 18
    +40/4DIFNBʹ͍ͭͯΑΓৄ͘͠஌Γ͍ͨํ΁
    +40/4DIFNBͰ͸͍͔ͭ͘όʔδϣϯ͕͋Γɺ
    ৽͍͠ %SBGUͰ͸ *G5IFO&MTFͱ͍͏ॻ͖ํ΋αϙʔτ͞Ε͍ͯ·͢
    ͦͷଞʹ΋ NVMUJQMF0G΍ QBUUFSO ͳͲܕ΍੍໿ͷදݱํ๏͕͋ͬͨΓ
    EFGJOJUJPOTͱ͍ͬͨศརͳߏจ΋ଘࡏ͠·͢
    ৄࡉ͸ 6OEFSTUBOEJOH+40/4DIFNB ͕ࢀߟʹͳΔͱࢥ͍·͢

    View Slide

  19. 19
    +40/4DIFNBपΓͷϥΠϒϥϦબఆ
    ΫοΫύουϚʔτͷൢചऀ޲͚؅ཧը໘ͷٕज़ελοΫ
    • ϑϩϯτΤϯυɿ 3BJMTͷ 7JFXͱ 3FBDU5ZQF4DSJQU
    • όοΫΤϯυɿ 3VCZPO3BJMT

    View Slide

  20. 20
    +40/4DIFNBपΓͷϥΠϒϥϦબఆ
    ΫοΫύουϚʔτͷൢചऀ޲͚؅ཧը໘ͷٕज़ελοΫ
    • ϑϩϯτΤϯυɿ 3BJMTͷ 7JFXͱ 3FBDU5ZQF4DSJQU
    • όοΫΤϯυɿ 3VCZPO3BJMT
    બఆͨ͠ϥΠϒϥϦ
    • ϑϩϯτΤϯυɿ SKTGUFBNSFBDUKTPOTDIFNBGPSN
    • όοΫΤϯυɿ [email protected]

    View Slide

  21. 21
    [email protected]
    JSONSchemer.schema(json_schema).valid?(json_to_be_validated)
    +40/4DIFNBΛར༻ͯ͠όϦσʔγϣϯΛ͢ΔͨΊͷ 3VCZ੡ϥΠϒϥϦ
    ͨͬͨ͜Ε͚ͩͰ +40/4DIFNBʹ΋ͱ͍ͮͨόϦσʔγϣϯ͕Ͱ͖Δʂ

    View Slide

  22. 22
    SFBDUKTPOTDIFNBGPSN
    import Form from 'react-jsonschema-form';
    export function App() {
    return
    }
    +40/4DIFNBΛར༻ͯ͠ೖྗϑΥʔϜΛࣗಈੜ੒͢Δ 3FBDUϥΠϒϥϦ
    +40/4DIFNBΛ 1SPQTͱͯ͠౉͚ͩ͢ͰɺೖྗϑΥʔϜ͕׬੒͢Δ
    POF0GͰઃఆͨ͠ೖྗϑΥʔϜͷग़͠෼͚΋ಈతʹ΍ͬͯ͘ΕΔʂ

    View Slide

  23. 23
    σβΠϯ͸ࣗ෼Ͱ࣮૷͢Δ
    • SFBDUKTPOTDIFNBGPSN͸ .BUFSJBM6*΍ #PPUTUSBQͳͲΛඪ४Ͱαϙʔτ͍ͯ͠Δ
    • ཁૉͷΧελϚΠζʹ͸ 8JEHFU΍ 'JFMEͳͲͷ࢓૊ΈΛར༻͢Δ

    View Slide

  24. 24
    σβΠϯ͸ࣗ෼Ͱ࣮૷͢Δ
    • SFBDUKTPOTDIFNBGPSN͸ .BUFSJBM6*΍ #PPUTUSBQͳͲΛඪ४Ͱαϙʔτ͍ͯ͠Δ
    • ཁૉͷΧελϚΠζʹ͸ 8JEHFU΍ 'JFMEͳͲͷ࢓૊ΈΛར༻͢Δ
    8JEHFUJOQVUཁૉ΍ TFMFDUཁૉࣗମ

    View Slide

  25. 25
    σβΠϯ͸ࣗ෼Ͱ࣮૷͢Δ
    • SFBDUKTPOTDIFNBGPSN͸ .BUFSJBM6*΍ #PPUTUSBQͳͲΛඪ४Ͱαϙʔτ͍ͯ͠Δ
    • ཁૉͷΧελϚΠζʹ͸ 8JEHFU΍ 'JFMEͳͲͷ࢓૊ΈΛར༻͢Δ
    8JEHFUJOQVUཁૉ΍ TFMFDUཁૉࣗମ 'JFME8JEHFUΛϥοϓ͢Δ $PNQPOFOU
    λΠτϧɾઆ໌ɾ஫هͳͲΛؚΊͨ಺༰

    View Slide

  26. 26
    8JEHFUͱ 'JFMEͷαϯϓϧίʔυ
    export const RadioWidget = (props) => (

    {props.options.enumOptions.map((option, i) => {
    return (


    disabled={props.disabled}
    type="radio"
    name={props.options.name}
    value={option.value}
    onChange={() => {
    props.onChange(option.value);
    }}
    />
    {option.label}


    );
    })}

    );
    export const FieldTemplate = (props) => {
    return (



    {props.required ? (
    必須
    ) : (
    任意
    )}
    {props.label}



    {props.rawDescription &&
    {props.rawDescription}}
    {props.children} {/* この部分で Widget の描画が⾏
    われる */}
    {props.rawHelp &&
    {props.rawHelp}}


    );
    };

    View Slide

  27. 27
    VJ4DIFNB
    export function App() {
    const uiSchema = {
    farmed: {
    'ui:disabled': false,
    'ui:name': 'item[farmed]’,
    'ui:widget': RadioWidget,
    'ui:help': '養殖か天然を必ず選択してください。’,
    },
    // ...
    };
    return (
    schema={jsonSchema}
    uiSchema={uiSchema}
    fieldTemplate={FieldTemplate}
    />
    );
    }
    +40/ 4DIFNB͸σʔλߏ଄ɾ੍໿ͷఆٛΛߦͳ͍ͬͯΔͨΊɺ
    ͦͷσʔλΛͲ͏͍͏෩ʹදࣔ͢Δ͔ʁͷఆٛͷ੹຿Λ VJ4DIFNB ͕୲͍ͬͯΔ

    View Slide

  28. 28
    +40/4DIFNBͰೖྗϑΥʔϜͷग़͠෼͚Λ࣮૷ͨ݁͠Ռ
    ඼࣭อূʹؔ͢Δೖྗෆඋ཰
    ঎඼ͷछྨΛબͿ͚ͩͰ඼࣭อূͷछྨ͕ࣗಈબ୒͞ΕΔ
    Α͏ʹͳͬͨͨΊɺೖྗෆඋ͸΄΅θϩʹͳΓ·ͨ͠
    ։ൃϝϯςφϯεͷ༰қੑ
    ঎඼ͷछྨͷ௥Ճ΍ཁ͕݅૿͑ͨͱͯ͠΋ɺ+40/4DIFNBͷఆٛΛߋ৽͢Δ͚ͩͰղܾ͠ɺ
    ೖྗϑΥʔϜͷग़͠෼੍͚ޚͱόοΫΤϯυͷόϦσʔγϣϯΛ༰қʹ௥Ճɾߋ৽Մೳͳঢ়ଶ

    View Slide

  29. 29
    Ԙग़ ݚ࢙
    ৯Ԙग़ݱ !TPMU

    View Slide