[{"data":1,"prerenderedAt":886},["ShallowReactive",2],{"h1290415216":3,"h3405042213":536,"h1857173168":879},{"id":4,"title":5,"body":6,"description":28,"extension":526,"meta":527,"navigation":531,"path":532,"seo":533,"stem":534,"__hash__":535},"docs\u002Fja\u002Ftips\u002F2026\u002F06\u002F11\u002Ftext-v2-message-design-history.md","Messaging APIのテキストメッセージ（v2）のシンタックスが決まるまで",{"type":7,"value":8,"toc":512},"minimark",[9],[10,11,12,20,25,29,43,46,49,54,57,241,259,272,279,282,315,326,341,348,365,398,404,427,449,462,473,492,496,499,502,506],"Tips",{},[13,14,17],"h1",{"id":15,"class":16},"","!mb-4",[18,19],"page-title",{},[21,22],"display-date",{"date":23,"class":24},"2026\u002F06\u002F11","!mb-20",[26,27,28],"p",{},"こんにちは。Messaging APIの開発を担当しているエンジニアの羽原です。",[26,30,31,32,37,38,42],{},"Messaging APIには、メンションやLINE絵文字を簡単に埋め込むことができる",[33,34,36],"a",{"href":35},"\u002Freference\u002Fmessaging-api\u002F#text-message-v2","テキストメッセージ（v2）","というメッセージタイプがあります。従来の",[33,39,41],{"href":40},"\u002Freference\u002Fmessaging-api\u002F#text-message","テキストメッセージ","と異なり、メンションや絵文字を入れる位置を本文の中で直接指定できるのが特徴です。",[26,44,45],{},"この記事では、テキストメッセージ（v2）のシンタックスが現在の形になるまでに行った設計判断を紹介します。",[47,48],"toc",{},[50,51,53],"h2",{"id":52},"textv2-syntax","テキストメッセージ（v2）のシンタックス",[26,55,56],{},"設計の話に入る前に、まずテキストメッセージ（v2）のシンタックスをおさらいしておきます。",[58,59,63],"pre",{"className":60,"code":61,"language":62,"meta":15,"style":15},"language-json shiki shiki-themes github-dark-default","{\n  \"type\": \"textV2\",\n  \"text\": \"Hello {you}! {smile}\",\n  \"substitution\": {\n    \"you\": {\n      \"type\": \"mention\",\n      \"mentionee\": {\n        \"type\": \"user\",\n        \"userId\": \"U0123456789ab...\"\n      }\n    },\n    \"smile\": {\n      \"type\": \"emoji\",\n      \"productId\": \"...\",\n      \"emojiId\": \"...\"\n    }\n  }\n}\n","json",[64,65,66,75,92,105,114,122,135,143,156,167,173,179,187,199,212,223,229,235],"code",{"__ignoreMap":15},[67,68,71],"span",{"class":69,"line":70},"line",1,[67,72,74],{"class":73},"sZEs4","{\n",[67,76,78,82,85,89],{"class":69,"line":77},2,[67,79,81],{"class":80},"sPWt5","  \"type\"",[67,83,84],{"class":73},": ",[67,86,88],{"class":87},"s9uIt","\"textV2\"",[67,90,91],{"class":73},",\n",[67,93,95,98,100,103],{"class":69,"line":94},3,[67,96,97],{"class":80},"  \"text\"",[67,99,84],{"class":73},[67,101,102],{"class":87},"\"Hello {you}! {smile}\"",[67,104,91],{"class":73},[67,106,108,111],{"class":69,"line":107},4,[67,109,110],{"class":80},"  \"substitution\"",[67,112,113],{"class":73},": {\n",[67,115,117,120],{"class":69,"line":116},5,[67,118,119],{"class":80},"    \"you\"",[67,121,113],{"class":73},[67,123,125,128,130,133],{"class":69,"line":124},6,[67,126,127],{"class":80},"      \"type\"",[67,129,84],{"class":73},[67,131,132],{"class":87},"\"mention\"",[67,134,91],{"class":73},[67,136,138,141],{"class":69,"line":137},7,[67,139,140],{"class":80},"      \"mentionee\"",[67,142,113],{"class":73},[67,144,146,149,151,154],{"class":69,"line":145},8,[67,147,148],{"class":80},"        \"type\"",[67,150,84],{"class":73},[67,152,153],{"class":87},"\"user\"",[67,155,91],{"class":73},[67,157,159,162,164],{"class":69,"line":158},9,[67,160,161],{"class":80},"        \"userId\"",[67,163,84],{"class":73},[67,165,166],{"class":87},"\"U0123456789ab...\"\n",[67,168,170],{"class":69,"line":169},10,[67,171,172],{"class":73},"      }\n",[67,174,176],{"class":69,"line":175},11,[67,177,178],{"class":73},"    },\n",[67,180,182,185],{"class":69,"line":181},12,[67,183,184],{"class":80},"    \"smile\"",[67,186,113],{"class":73},[67,188,190,192,194,197],{"class":69,"line":189},13,[67,191,127],{"class":80},[67,193,84],{"class":73},[67,195,196],{"class":87},"\"emoji\"",[67,198,91],{"class":73},[67,200,202,205,207,210],{"class":69,"line":201},14,[67,203,204],{"class":80},"      \"productId\"",[67,206,84],{"class":73},[67,208,209],{"class":87},"\"...\"",[67,211,91],{"class":73},[67,213,215,218,220],{"class":69,"line":214},15,[67,216,217],{"class":80},"      \"emojiId\"",[67,219,84],{"class":73},[67,221,222],{"class":87},"\"...\"\n",[67,224,226],{"class":69,"line":225},16,[67,227,228],{"class":73},"    }\n",[67,230,232],{"class":69,"line":231},17,[67,233,234],{"class":73},"  }\n",[67,236,238],{"class":69,"line":237},18,[67,239,240],{"class":73},"}\n",[26,242,243,246,247,250,251,254,255,258],{},[64,244,245],{},"text","の中に",[64,248,249],{},"{you}","や",[64,252,253],{},"{smile}","のようなプレースホルダーを書き、",[64,256,257],{},"substitution","プロパティでそれぞれの中身（メンションやLINE絵文字の情報）を指定する形式です。",[26,260,261,262,264,265,267,268,271],{},"このシンタックスでは、置換内容を",[64,263,257],{},"プロパティに分けて指定します。また、既存の",[64,266,245],{},"タイプを拡張するのではなく、新しく",[64,269,270],{},"textV2","タイプとして定義しています。ここからは、この形に至るまでの設計判断を1つずつ振り返っていきます。",[50,273,275,276,278],{"id":274},"why-substitution-property","なぜ",[64,277,257],{},"プロパティで置換する形にしたのか",[26,280,281],{},"最初の候補として挙がっていたのは、メンションや絵文字に必要な情報をテキストの中だけで完結させる形式でした。たとえば次のようなシンタックスです。",[58,283,285],{"className":60,"code":284,"language":62,"meta":15,"style":15},"{\n  \"type\": \"text\",\n  \"text\": \"Hello {@U0123456789ab...}! {:smile:}\"\n}\n",[64,286,287,291,302,311],{"__ignoreMap":15},[67,288,289],{"class":69,"line":70},[67,290,74],{"class":73},[67,292,293,295,297,300],{"class":69,"line":77},[67,294,81],{"class":80},[67,296,84],{"class":73},[67,298,299],{"class":87},"\"text\"",[67,301,91],{"class":73},[67,303,304,306,308],{"class":69,"line":94},[67,305,97],{"class":80},[67,307,84],{"class":73},[67,309,310],{"class":87},"\"Hello {@U0123456789ab...}! {:smile:}\"\n",[67,312,313],{"class":69,"line":107},[67,314,240],{"class":73},[26,316,317,318,321,322,325],{},"しかし、この形式は採用を見送りました。その理由は、シンタックスの誤用によってユーザーIDが露出するような事態を避けるためです。たとえば",[64,319,320],{},"{}","を入れ忘れて",[64,323,324],{},"Hello @U0123456789ab...!","と送ってしまうと、本来メンションになるはずだった生のユーザーIDがそのまま本文として相手のトークに表示されてしまいます。LINE公式アカウントは、企業や店舗の顧客とのコミュニケーション手段としてご利用いただいているプロダクトです。そのため、保守的で事故を起こしにくい設計にする必要があったのです。",[26,327,328,329,331,332,334,335,337,338,340],{},"そこで採用したのが、冒頭で示した",[64,330,257],{},"プロパティによる置換方式です。この方式であれば、ユーザーIDのような取り扱いに注意が必要な情報を",[64,333,257],{},"の中だけで扱うことができます。そのため、",[64,336,245],{},"側でtypoしても相手に見えるのは",[64,339,249],{},"のようなプレースホルダー名にとどまり、生のユーザーIDが本文に露出する事態を避けやすい構造になっています。",[50,342,344,345,347],{"id":343},"why-new-type","なぜ新しく",[64,346,270],{},"というタイプを設けたのか",[26,349,350,351,353,354,357,358,361,362,364],{},"当初は新しいタイプを作らず、既存の",[64,352,245],{},"タイプのまま機能を拡張する案も検討していました。しかし、プレースホルダーを扱うには",[64,355,356],{},"{","と",[64,359,360],{},"}","を制御文字として扱う必要があります。一方で、既存の",[64,363,245],{},"タイプでそのルールを導入すると後方互換性を保てないという問題に行き当たります。",[26,366,367,357,369,371,372,374,375,378,379,382,383,386,387,389,390,393,394,397],{},[64,368,356],{},[64,370,360],{},"は制御文字として扱うため、これらをそのままテキストとして送信したい場合に備えて、",[64,373,270],{},"タイプではPythonの",[64,376,377],{},"str.format","で使われているエスケープシンタックス（",[64,380,381],{},"{{","、",[64,384,385],{},"}}","）を採用しました。もしこのルールを",[64,388,245],{},"タイプに適用してしまうと、これまで",[64,391,392],{},"{{example}}","という文字列をそのまま送信していた開発者の実装では、テキストの内容が",[64,395,396],{},"{example}","に変わってしまいます。既存のコードの挙動を変えてしまうわけにはいきません。",[26,399,400,401,403],{},"つまり、",[64,402,245],{},"タイプには次の2つのモードが必要になります。",[405,406,407,420],"ul",{},[408,409,410,357,412,414,415,250,417,419],"li",{},[64,411,356],{},[64,413,360],{},"を制御文字として扱い、文字として使う場合は",[64,416,381],{},[64,418,385],{},"にエスケープが必要なモード",[408,421,422,357,424,426],{},[64,423,356],{},[64,425,360],{},"を単なる文字として扱う従来のモード",[26,428,429,430,432,433,435,436,438,439,441,442,445,446,448],{},"この2つを区別する方法として、",[64,431,257],{},"プロパティの有無で切り替える案が考えられます。新しいモードを使う開発者は",[64,434,257],{},"を指定しているはずで、一方既存のコードはこのプロパティを使っていないため、自然な発想と言えます。ただし、",[64,437,257],{},"の状態を「要素あり」「",[64,440,320],{},"（空オブジェクト）」「",[64,443,444],{},"null","または未指定」の3パターンに分けたとき、真ん中の",[64,447,320],{},"をどちらに分類するかが問題になります。",[26,450,451,453,454,357,456,458,459,461],{},[64,452,320],{},"を新モード側として扱えば、",[64,455,320],{},[64,457,444],{},"で挙動が分かれることになります。この場合、空オブジェクトと",[64,460,444],{},"をどちらも空の値として扱う言語やフレームワークでは、開発者の意図とは違うモードで送信されるおそれがあります。これは、JSONベースのAPIを設計するときに注意が必要な罠の1つです。",[26,463,464,465,467,468,250,470,472],{},"逆に",[64,466,320],{},"を従来モード側として扱う場合、置換要素数が0個のときには",[64,469,356],{},[64,471,360],{},"のエスケープ処理を例外的に行わないか、ダミー要素を入れて新モードにするかの対応が必要になります。いずれの場合も、使い方に本来不要なはずの工夫が求められることになります。",[26,474,475,476,478,479,482,483,485,486,488,489,491],{},"どちらの分類でも問題が残るため、",[64,477,257],{},"プロパティの有無でモードを切り替える方式は諦め、モードを",[64,480,481],{},"type","の段階で固定することにしました。これが、",[64,484,270],{},"を新しいタイプとして切り出した最大の理由です。",[64,487,245],{},"タイプの挙動は従来のまま保ちつつ、",[64,490,270],{},"タイプでは後方互換性に縛られずに置換シンタックスを設計できるようになりました。",[50,493,495],{"id":494},"wrap-up","おわりに",[26,497,498],{},"シンタックス1つを決めるのにも、利便性以外に誤用時のリスクや既存コードへの影響、JSONで表現できる値をAPIとしてどう解釈するかといった複数の観点を整理する必要がありました。",[26,500,501],{},"普段は表に出ないAPI設計の裏話でしたが、少しでも楽しんでいただけたなら幸いです。",[503,504,505],"style",{},"html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",[507,508],"tags",{"tags":509,"lang":510,"section":511},"messaging-api","en","tips",{"title":15,"searchDepth":107,"depth":107,"links":513},[514,517,520,523],{"id":515,"depth":77,"text":516},"テキストメッセージv2のシンタックス-textv2-syntax","テキストメッセージ（v2）のシンタックス {#textv2-syntax}",{"id":518,"depth":77,"text":519},"なぜsubstitutionプロパティで置換する形にしたのか-why-substitution-property","なぜsubstitutionプロパティで置換する形にしたのか {#why-substitution-property}",{"id":521,"depth":77,"text":522},"なぜ新しくtextv2というタイプを設けたのか-why-new-type","なぜ新しくtextV2というタイプを設けたのか {#why-new-type}",{"id":524,"depth":77,"text":525},"おわりに-wrap-up","おわりに {#wrap-up}","md",{"date":528,"tags":509,"locale":529,"sidebar":530},"2026-06-11 00:00 UTC","ja",false,true,"\u002Fja\u002Ftips\u002F2026\u002F06\u002F11\u002Ftext-v2-message-design-history",{"title":5,"description":28},"ja\u002Ftips\u002F2026\u002F06\u002F11\u002Ftext-v2-message-design-history","iKUclHoZJGQmxR-o0D93xAd9oJChwwdl3Sd6E4ElEUY",{"id":4,"title":5,"body":537,"description":28,"extension":526,"meta":877,"navigation":531,"path":532,"seo":878,"stem":534,"__hash__":535},{"type":7,"value":538,"toc":871},[539],[10,540,541,545,547,549,555,557,559,561,563,691,701,709,713,715,745,751,761,765,775,795,799,817,831,841,849,861,863,865,867,869],{},[13,542,543],{"id":15,"class":16},[18,544],{},[21,546],{"date":23,"class":24},[26,548,28],{},[26,550,31,551,37,553,42],{},[33,552,36],{"href":35},[33,554,41],{"href":40},[26,556,45],{},[47,558],{},[50,560,53],{"id":52},[26,562,56],{},[58,564,565],{"className":60,"code":61,"language":62,"meta":15,"style":15},[64,566,567,571,581,591,597,603,613,619,629,637,641,645,651,661,671,679,683,687],{"__ignoreMap":15},[67,568,569],{"class":69,"line":70},[67,570,74],{"class":73},[67,572,573,575,577,579],{"class":69,"line":77},[67,574,81],{"class":80},[67,576,84],{"class":73},[67,578,88],{"class":87},[67,580,91],{"class":73},[67,582,583,585,587,589],{"class":69,"line":94},[67,584,97],{"class":80},[67,586,84],{"class":73},[67,588,102],{"class":87},[67,590,91],{"class":73},[67,592,593,595],{"class":69,"line":107},[67,594,110],{"class":80},[67,596,113],{"class":73},[67,598,599,601],{"class":69,"line":116},[67,600,119],{"class":80},[67,602,113],{"class":73},[67,604,605,607,609,611],{"class":69,"line":124},[67,606,127],{"class":80},[67,608,84],{"class":73},[67,610,132],{"class":87},[67,612,91],{"class":73},[67,614,615,617],{"class":69,"line":137},[67,616,140],{"class":80},[67,618,113],{"class":73},[67,620,621,623,625,627],{"class":69,"line":145},[67,622,148],{"class":80},[67,624,84],{"class":73},[67,626,153],{"class":87},[67,628,91],{"class":73},[67,630,631,633,635],{"class":69,"line":158},[67,632,161],{"class":80},[67,634,84],{"class":73},[67,636,166],{"class":87},[67,638,639],{"class":69,"line":169},[67,640,172],{"class":73},[67,642,643],{"class":69,"line":175},[67,644,178],{"class":73},[67,646,647,649],{"class":69,"line":181},[67,648,184],{"class":80},[67,650,113],{"class":73},[67,652,653,655,657,659],{"class":69,"line":189},[67,654,127],{"class":80},[67,656,84],{"class":73},[67,658,196],{"class":87},[67,660,91],{"class":73},[67,662,663,665,667,669],{"class":69,"line":201},[67,664,204],{"class":80},[67,666,84],{"class":73},[67,668,209],{"class":87},[67,670,91],{"class":73},[67,672,673,675,677],{"class":69,"line":214},[67,674,217],{"class":80},[67,676,84],{"class":73},[67,678,222],{"class":87},[67,680,681],{"class":69,"line":225},[67,682,228],{"class":73},[67,684,685],{"class":69,"line":231},[67,686,234],{"class":73},[67,688,689],{"class":69,"line":237},[67,690,240],{"class":73},[26,692,693,246,695,250,697,254,699,258],{},[64,694,245],{},[64,696,249],{},[64,698,253],{},[64,700,257],{},[26,702,261,703,264,705,267,707,271],{},[64,704,257],{},[64,706,245],{},[64,708,270],{},[50,710,275,711,278],{"id":274},[64,712,257],{},[26,714,281],{},[58,716,717],{"className":60,"code":284,"language":62,"meta":15,"style":15},[64,718,719,723,733,741],{"__ignoreMap":15},[67,720,721],{"class":69,"line":70},[67,722,74],{"class":73},[67,724,725,727,729,731],{"class":69,"line":77},[67,726,81],{"class":80},[67,728,84],{"class":73},[67,730,299],{"class":87},[67,732,91],{"class":73},[67,734,735,737,739],{"class":69,"line":94},[67,736,97],{"class":80},[67,738,84],{"class":73},[67,740,310],{"class":87},[67,742,743],{"class":69,"line":107},[67,744,240],{"class":73},[26,746,317,747,321,749,325],{},[64,748,320],{},[64,750,324],{},[26,752,328,753,331,755,334,757,337,759,340],{},[64,754,257],{},[64,756,257],{},[64,758,245],{},[64,760,249],{},[50,762,344,763,347],{"id":343},[64,764,270],{},[26,766,350,767,353,769,357,771,361,773,364],{},[64,768,245],{},[64,770,356],{},[64,772,360],{},[64,774,245],{},[26,776,777,357,779,371,781,374,783,378,785,382,787,386,789,389,791,393,793,397],{},[64,778,356],{},[64,780,360],{},[64,782,270],{},[64,784,377],{},[64,786,381],{},[64,788,385],{},[64,790,245],{},[64,792,392],{},[64,794,396],{},[26,796,400,797,403],{},[64,798,245],{},[405,800,801,811],{},[408,802,803,357,805,414,807,250,809,419],{},[64,804,356],{},[64,806,360],{},[64,808,381],{},[64,810,385],{},[408,812,813,357,815,426],{},[64,814,356],{},[64,816,360],{},[26,818,429,819,432,821,435,823,438,825,441,827,445,829,448],{},[64,820,257],{},[64,822,257],{},[64,824,257],{},[64,826,320],{},[64,828,444],{},[64,830,320],{},[26,832,833,453,835,357,837,458,839,461],{},[64,834,320],{},[64,836,320],{},[64,838,444],{},[64,840,444],{},[26,842,464,843,467,845,250,847,472],{},[64,844,320],{},[64,846,356],{},[64,848,360],{},[26,850,475,851,478,853,482,855,485,857,488,859,491],{},[64,852,257],{},[64,854,481],{},[64,856,270],{},[64,858,245],{},[64,860,270],{},[50,862,495],{"id":494},[26,864,498],{},[26,866,501],{},[503,868,505],{},[507,870],{"tags":509,"lang":510,"section":511},{"title":15,"searchDepth":107,"depth":107,"links":872},[873,874,875,876],{"id":515,"depth":77,"text":516},{"id":518,"depth":77,"text":519},{"id":521,"depth":77,"text":522},{"id":524,"depth":77,"text":525},{"date":528,"tags":509,"locale":529,"sidebar":530},{"title":5,"description":28},[880,881,883,885],{"id":52,"depth":77,"text":53},{"id":274,"depth":77,"text":882},"なぜsubstitutionプロパティで置換する形にしたのか",{"id":343,"depth":77,"text":884},"なぜ新しくtextV2というタイプを設けたのか",{"id":494,"depth":77,"text":495},1781161619558]