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

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

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for solt9029 solt9029
October 22, 2021

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

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

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

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

Avatar for solt9029

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 ͕୲͍ͬͯΔ