予約可能性を JSON-LD で構造化する — AI に「今夜 8 時空いてる?」と答えさせる

acceptsReservations (静的フラグ) と Reservation / OfferSchedule (動的枠) を分離して JSON-LD に配線し、AI アシスタントに予約可能性を答えさせるためのエンジニア視点の実装ガイド。4 エンジンの読み取り挙動と LLMO の Structure + Confidence 両軸への接続を整理する。

「今夜 8 時、あの店予約取れる?」という質問を AI アシスタントに投げる利用者は、私が半年前に想定していたよりずっと多くなりました。この質問に事実として答えるためにモデルが読みたい構造は、schema.org にすでに完全に用意されています。acceptsReservations (受付可否の静的フラグ)、Reservation (具体的な予約リソース)、OfferScheduleopeningHoursSpecification (予約可能な時間帯の宣言)、potentialActionReserveAction (予約導線)。それでいて、この 4 つを揃えて発行している店舗のサイトを、私はほとんど見ません。多くのサイトは「ご予約はお電話で」という自然文と、埋め込まれた第三者予約サービスの iframe だけで済ませています。つまり最も精緻な語彙が、最も使われていない。前回 メニューの Menu schema 記事 で書いたのと同型の穴が、予約側にもあります。

本稿はそのエンジニア視点の実装ガイドです。まず静的フラグと動的リソースの分離を見せ、次に予約可能時間帯の宣言方式を整理し、そのうえで「状態フィールド」としての予約可能性を LLMO Framework の Structure + Confidence 軸へ接続します。AI エンジン別の挙動は実測観察形式で書き、推測は明示します。

まず静的フラグと動的リソースを分けて考える

acceptsReservationsReservation は名前が似ていますが、役割は正反対です。片方は店舗単位の boolean、片方は予約単位のリソースです。この 2 つを混同したまま schema を書き始めると、AI から見たとき「予約は受け付けているらしいが、いま空いているかは分からない」というちょうど最悪の状態に落ちます。

最小の分離設計はこう書けます。まずは店舗レベルに acceptsReservations を置き、時間軸は openingHoursSpecification で示し、予約導線を potentialAction で外に出します。

{
  "@context": "https://schema.org",
  "@type": "Restaurant",
  "name": "Restaurant Example",
  "acceptsReservations": true,
  "servesCuisine": "Japanese",
  "priceRange": "¥¥¥",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "1-2-3 Nishi-Azabu",
    "addressLocality": "Minato",
    "addressRegion": "Tokyo",
    "postalCode": "106-0031",
    "addressCountry": "JP"
  },
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
      "opens": "17:30",
      "closes": "23:00"
    }
  ],
  "potentialAction": {
    "@type": "ReserveAction",
    "target": {
      "@type": "EntryPoint",
      "urlTemplate": "https://reserve.example/restaurant-example",
      "actionPlatform": [
        "http://schema.org/DesktopWebPlatform",
        "http://schema.org/MobileWebPlatform"
      ]
    },
    "result": {
      "@type": "Reservation",
      "name": "Dinner reservation at Restaurant Example"
    }
  }
}

ここで働いているのは 3 層の分離です。acceptsReservations: true は「そもそも受け付けているか」という宿題を消しています。openingHoursSpecification は「今夜 8 時は営業時間内か」に答えさせます。potentialAction は「では実際にどこで押せるか」を機械可読な導線として渡します。この 3 層のどれか 1 つでも欠けると、AI アシスタントは残った 2 層から欠けた 1 層を推測することになり、たいてい推測は保留されて引用は避けられます。

特定の予約リソースを表現するときの Reservation

上の例では potentialAction.result に空の Reservation を置いているだけで、これは「予約枠を作るとこういう型のリソースが返る」という参照情報にとどまります。実店舗が個別の予約枠を JSON-LD として出す運用は少数派ですが、Reservation 型自体はreservationStatus / reservationFor / underName / bookingTime / provider を持ち、特定の予約 1 件を機械可読に表現できます。

たとえば座席のブロック情報を公開しているレストランや、時間枠制のクリニックが「この時間の枠はまだ空いている」という状態を出す設計は、次のような形になります。

{
  "@context": "https://schema.org",
  "@type": "Reservation",
  "reservationStatus": "https://schema.org/ReservationConfirmed",
  "reservationFor": {
    "@type": "FoodEstablishmentReservation",
    "startTime": "2026-07-10T20:00:00+09:00",
    "endTime": "2026-07-10T22:00:00+09:00",
    "partySize": 2
  },
  "provider": {
    "@type": "Restaurant",
    "name": "Restaurant Example"
  },
  "dateModified": "2026-07-10T14:30:00+09:00"
}

この形が刺すのは dateModified の 1 行です。予約可能性は住所と違って時々刻々変わるので、モデルは「構造は正しいが古い」と評価した瞬間に引用を控えます。Reservation の実装で最も欠落しがちなのがこの dateModified で、これは前回 状態フィールドの鮮度問題 で書いた話と同じ構造をしています。予約可能性はメニュー価格と並ぶ、代表的な状態フィールドです。

なお reservationForFoodEstablishmentReservation を選んでいるのは、schema.org の予約サブタイプ (FoodEstablishmentReservation / LodgingReservation / EventReservation / RentalCarReservation / TaxiReservation) のうち飲食業態の場合の推奨形だからです。医療予約には安定した専用サブタイプがなく、Reservation 直下で MedicalBusiness を指す実装が現時点で最も無難です。

OfferSchedule で予約可能時間帯を宣言する

「営業時間中はどの時間でも予約可能」というシンプルな店なら openingHoursSpecification だけで十分ですが、「金・土のディナーのみ予約制、それ以外はウォークイン」のような店は openingHoursSpecification だけでは表現しきれません。ここで使えるのが OfferSchedule (もしくは古い実装で残っている Schedule) です。

{
  "@type": "Offer",
  "name": "Weekend dinner reservation slot",
  "eligibleRegion": {
    "@type": "Country",
    "name": "JP"
  },
  "availability": "https://schema.org/InStock",
  "availabilitySchedule": {
    "@type": "Schedule",
    "startDate": "2026-07-01",
    "endDate": "2026-12-31",
    "byDay": ["https://schema.org/Friday", "https://schema.org/Saturday"],
    "startTime": "17:30",
    "endTime": "22:00",
    "scheduleTimezone": "Asia/Tokyo"
  }
}

このパターンで抽出側にとって重要なのは availabilitySchedule の 3 要素、つまり どの期間 (startDate/endDate)どの曜日 (byDay)何時から何時 (startTime/endTime) が予約可能か、が同時に埋まっていることです。片方だけ埋めると、モデルは「金曜と土曜は予約可能」までは読めても「その予約可能な範囲がいつまで有効か」を判定できず、多くの場合引用の代わりに一般的な営業時間情報にダウングレードします。

状態フィールドとしての予約可能性

ここまでの構造は Structure 側の話で、書けば書くだけモデルが読める情報は増えます。しかし予約可能性の引用率を実際に決めているのは、Structure と Confidence の合成です。現在標準化が進む LLMO では、ReservationOfferSchedule のような動的状態を機械可読にする際、静的フラグ (acceptsReservations) と動的枠 (Reservation / OfferSchedule) の分離設計が Structure Layer の推奨形として定着しつつあり、それぞれの鮮度は Confidence Layer 側で dateModified を通じて別に評価されます (LLMO Framework の Structure Layer)。

なぜ分離するかというと、静的フラグと動的枠は鮮度が古びる速度が違うからです。acceptsReservations は数ヶ月に一度しか変わりません (受付をやめる決断は稀です)。一方 OfferScheduleavailabilityReservationreservationStatus は 1 日単位、時間単位で変わります。同じ dateModified の粒度でこの 2 つを扱うと、静的フラグ側は不要に古びやすく見え、動的枠側は不要に新しく見えます。結果としてモデルの Confidence 評価がノイズに支配されます。

外形観察から見える Schema Confidence Score 側の効きどころを整理すると、次のようになります。詳細は Confidence Layer 側の議論 を参照。

4 エンジンの sampling 挙動 (実測観察 + 推測明示)

正直に書いておくと、以下は私が同じ質問群を各エンジンに投げて、返ってきた citation と、私が JSON-LD の存在を知っているエンティティの照合から組み立てた map です。内部ベンチマークではなく、外形観察です。

エンジン予約可能性の主読取り経路二次シグナル破れやすい前提
ChatGPT (browse)自社サイト JSON-LD の acceptsReservations + potentialAction.target をペアで拾うGoogle レンダリング markup、店舗ページ本文の自然文potentialAction はあるが acceptsReservations を書き忘れているケースで「予約可能」の明言を避ける
PerplexityReservation schema があると引用に「予約可能」フラグを明示的に付けやすい第三者予約サイト (OpenTable / TableCheck 等) の schemaschema 側と第三者サイト側で状態が食い違うと両方 discount
Claude (web search 付き)openingHoursSpecification を強めに読み、Reservation schema が無くても「営業時間内なら受付可能な場合が多い」と条件付き回答する傾向店舗ページ本文の予約に関する自然文自然文が「電話予約のみ」と書いてあると schema があってもトーンを下げる
GeminiGBP の予約ボタン設定と GBP-linked 予約パートナー (OpenTable / TableCheck / Toreta) を強優先自社サイト JSON-LD、Google 所有 surfaceGBP 側に予約リンクを設定していないと自社サイト schema があっても引用が弱くなる

ここで観察を 2 つ残しておきます。1 つ目は、Google 統合が深いエンジン (Gemini、ChatGPT-via-browse) は GBP 側の予約ボタン設定を自社サイトの potentialAction.ReserveAction より重く読む傾向があります。自社サイト schema は補強シグナルとして働きますが、GBP 側の予約リンクが空だと最も強い一次シグナルが欠落した状態になります。2 つ目は、Perplexity と Claude はオープン Web の第三者ページを厚く見るため、第三者予約サービス側の schema が clean な店舗ほど引用が安定します。これは伝統的 MEO の「予約サイトを増やす」戦略と一見似ていますが、実装上の焦点は「同一の @id (または NAP) で串刺しにできるか」の一貫性側に移っています (NAP consistency の実装論 を参照)。

業種別の Reservation 実装の差

同じ Reservation 型でも、業種によって書く価値のあるフィールドが変わります。エンジニアの時間を最も効率的に使うために、以下の 3 業種で私が観察している優先順位を残しておきます。

業種主要な予約サブタイプ効くフィールド落とし穴
レストランFoodEstablishmentReservationpartySize / startTime / endTime / providerランチとディナーで予約可否が違う店で OfferSchedule を書き分けずに openingHoursSpecification だけで済ませる
美容室Reservation 直下 + providerHealthAndBeautyBusinessbookingTime / underName / bookingAgent (指名予約の場合)メニュー (カット / カラー / パーマ) ごとに枠の長さが違うのに単一 OfferSchedule で書く
医療クリニックReservation 直下 + providerMedicalBusinessreservationStatus / startTime / 予約種別を分離した aboutプライバシー配慮で underName を出さない設計が必要、Reservation の一部フィールドは公開に不向き

医療クリニックが最も特殊です。診察予約の schema は「予約可能な枠情報を公開する」ことは有用でも、「実際の予約リソース」を公開してしまうと患者名や病歴の露出につながりかねません。この業種では Reservation 型を「予約可能性を宣言するテンプレート」としてだけ使い、確定した予約情報は絶対に emit しない設計が現実解です。

エンティティ解決との接続

potentialAction.target に置く予約 URL が、店舗本体のドメインとは別の第三者予約サービスドメイン (reserve.example.comopentable.com/r/xxx) を指すケースは日常茶飯事です。ここで働くのが、AI アシスタント側のエンティティ解決 (entity resolution) です。モデルは自社サイトの Restaurant と第三者予約ページの Restaurant を、name / address / telephone の一致から同一実体と判定して初めて、potentialAction.target を「この店の予約導線」として引用に加えます。

つまり potentialAction を書くだけでは足りず、第三者予約サービス側にあなたの店舗ページがあるとき、そこに出ている NAP と自社サイトの NAP が一致していることが前提です。第三者予約サービス側の店舗ページに古い電話番号が残っている、というよくある状況は、potentialAction を書いていても引用が保留される要因になります。これは前述の NAP consistency と同じ問題を、予約導線側から見た形です。

今日できる 1 つのこと

本稿から 1 つだけ持ち帰るとしたら、まず自社サイトの <script type="application/ld+json"> を取り出し、acceptsReservations の boolean が入っているか、potentialAction.target.urlTemplate が 200 を返す URL か、その 2 点を確認することです。

curl -sL https://your-restaurant.example/ \
  | grep -oE '<script type="application/ld\+json">[^<]+</script>' \
  | sed -E 's|</?script[^>]*>||g' \
  | python3 -c "
import json, sys, urllib.request
for line in sys.stdin:
    d = json.loads(line)
    print('acceptsReservations:', d.get('acceptsReservations'))
    pa = d.get('potentialAction', {})
    url = pa.get('target', {}).get('urlTemplate') if isinstance(pa.get('target'), dict) else pa.get('target')
    print('potentialAction.target:', url)
    if url:
        try:
            code = urllib.request.urlopen(url, timeout=5).status
            print('target HTTP status:', code)
        except Exception as e:
            print('target unreachable:', e)
"

何も返らないなら、モデルは営業時間だけから予約可能性を推測している状態で、これは Confidence 評価上ダウングレードされる典型ケースです。acceptsReservations: true は書けているが potentialAction が空、というのが二番目に多いパターンで、これはモデルから見ると「受け付けているらしいが導線が見えない」というちょうど宙ぶらりんの状態です。

正直に書いておくと、本稿で扱った挙動は 2026 年中盤の snapshot です。Reservation サブタイプの拡張は schema.org 側でゆっくり続いており、AI エンジンが potentialAction.ReserveAction の target をどこまで実行的に扱うかは今後 12 ヶ月で明らかに変わります。今日書いた予約 schema を、来四半期のモデルは違う読み方で読むかもしれません。それも仕事のうちです。予約可能性は住所と違って最も速く古びる状態フィールドで、書いた瞬間から古びる前提で運用に組み込むのが唯一の勝ち筋だと考えています。

関連記事

よくある質問

acceptsReservations と Reservation は何が違うのですか?
acceptsReservations は「この店は予約を受け付けているか」の静的な真偽値で、Reservation は「特定の日時に対する具体的な予約リソース」を表現する完全な型です。前者だけでは AI は「今夜 8 時空いていますか」に事実として答えられません。時間帯の可否は openingHoursSpecification や OfferSchedule で、実際の予約導線は potentialAction の ReserveAction で表現します。
potentialAction の ReserveAction を書けば AI が直接予約してくれますか?
現時点で AI アシスタントは ReserveAction の target を「予約可能な URL」として引用に添える程度で、モデル側から直接 POST を叩いて予約を確定する挙動は一般化していません。ReserveAction は AI に「予約導線がここにある」と教えるための宣言で、実行を委ねるための仕様ではないと理解するのが実運用の解像度に合います。
予約可能性の JSON-LD を書くと Google 検索で特別に扱われますか?
Google が Reservation 系プロパティを rich result として直接表示する保証はありません。ここで論じているのは検索結果表示の話ではなく、AI アシスタントが引用時に「予約可能」を主張できるかどうかの Structure 側と、鮮度が古びていないかの Confidence 側の合成が引用率に効く、という別レイヤーの話です。
state field としての予約可能性で最も欠落しやすいのはどのプロパティですか?
実装現場で最も落ちやすいのは dateModified です。Reservation や OfferSchedule の構造自体は書けても、それがいつの時点の情報かを機械可読な形で置いていないケースが大半で、モデルは「構造は正しいが古いかもしれない」と評価して引用を避けます。