Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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. 2 Ԙग़ ݚ࢙ ৯Ԙग़ݱ !TPMU ΫοΫύουגࣜձࣾ ങ෺ϓϩμΫτ։ൃ෦ ॴଐ ීஈ͸ 3BJMT΍

    3FBDUΛॻ͘ झຯ͸ϓϩτλΠϐϯάʢ࠷ۙ͸͓ֆඳ͖΋޷͖ʣ ҿञ TVEPΛ๷ࢭ͢ΔγεςϜ
  2. 7 ঎඼৘ใͷొ࿥ • ͓ૣΊʹ͓ঌ্͕͠Γͩ͘ ͍͞ • ফඅظݶ·Ͱͷ೔਺ • ղౚ඼͔Ͳ͏͔ •

    ੜ৯༻͔Ճ೤༻͔ • ཆ৩͔ఱવ͔ • ফඅظݶ·Ͱͷ೔਺ • ղౚ඼͔Ͳ͏͔ ೖྗ͢΂͖඼࣭อূ΍৯඼දࣔʹؔΘΔ৘ใ͸঎඼ͷछྨ͝ͱʹҟͳΔ
  3. 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 จΛେྔʹ࢖ͬͯΈΔ
  4. 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: [ # 中略 ], }, }, } • ࠓޙɺ঎඼ͷछྨΛ௥Ճͨ͘͠ͳͬͨͱ͖ ͷϝϯςφϯεੑ͕ߴ͍ • ϑϩϯτΤϯυͱόοΫΤϯυͷϩδοΫ ͕ڞ௨ͷεΩʔϚʹΑΓ׬੒͢Δ 👍 ϝϦοτ
  5. 12 +40/4DIFNBͱ͸ { id: 100, name: "カルボナーラの作り方", content: "ベーコンと玉ねぎを食べやすい大き さに切ります。〜(以下略)",

    public: true } +40/4DIFNBͱ͸ +40/ͷߏ଄Λఆٛͨ͠΋ͷ ྉཧϨγϐͷ +40/ͷߏ଄Λఆٛͯ͠ΈΑ͏ +40/ͷྫ
  6. 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ͷྫ
  7. 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ͷྫ ܕͳͲ੍໿͕ఆٛͰ͖Δ
  8. 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 }, /** 魚介加工品独自の入力項目 **/ }}, ] }, }, }
  9. 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Ͱॻ͍ͯΈΔ
  10. 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Ͱॻ͍ͯΈΔ
  11. 20 +40/4DIFNBपΓͷϥΠϒϥϦબఆ ΫοΫύουϚʔτͷൢചऀ޲͚؅ཧը໘ͷٕज़ελοΫ • ϑϩϯτΤϯυɿ 3BJMTͷ 7JFXͱ 3FBDU5ZQF4DSJQU • όοΫΤϯυɿ

    3VCZPO3BJMT બఆͨ͠ϥΠϒϥϦ • ϑϩϯτΤϯυɿ SKTGUFBNSFBDUKTPOTDIFNBGPSN • όοΫΤϯυɿ EBWJTINDDMVSHKTPO@TDIFNFS
  12. 22 SFBDUKTPOTDIFNBGPSN import Form from 'react-jsonschema-form'; export function App() {

    return <Form schema={jsonSchema} /> } +40/4DIFNBΛར༻ͯ͠ೖྗϑΥʔϜΛࣗಈੜ੒͢Δ 3FBDUϥΠϒϥϦ +40/4DIFNBΛ 1SPQTͱͯ͠౉͚ͩ͢ͰɺೖྗϑΥʔϜ͕׬੒͢Δ POF0GͰઃఆͨ͠ೖྗϑΥʔϜͷग़͠෼͚΋ಈతʹ΍ͬͯ͘ΕΔʂ
  13. 26 8JEHFUͱ 'JFMEͷαϯϓϧίʔυ export const RadioWidget = (props) => (

    <div className="field-radio-group"> {props.options.enumOptions.map((option, i) => { return ( <div key={i}> <label> <input disabled={props.disabled} type="radio" name={props.options.name} value={option.value} onChange={() => { props.onChange(option.value); }} /> <span>{option.label}</span> </label> </div> ); })} </div> ); export const FieldTemplate = (props) => { return ( <Card> <Card.Header> <div> {props.required ? ( <Badge variant="primary">必須</Badge> ) : ( <Badge variant="secondary">任意</Badge> )} {props.label} </div> </Card.Header> <Card.Body> {props.rawDescription && <Card.Text>{props.rawDescription}</Card.Text>} {props.children} {/* この部分で Widget の描画が⾏ われる */} {props.rawHelp && <small>{props.rawHelp}</small>} </Card.Body> </Card> ); };
  14. 27 VJ4DIFNB export function App() { const uiSchema = {

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