JavaScript URL デコード — decodeURIComponent()

·Front-end & Node.js Developer·レビュー担当Marcus Webb·公開日

無料の URLデコーダー をブラウザで直接使用 — インストール不要。

URLデコーダー をオンラインで試す →

パーセントエンコードされた文字列は JavaScript コードに常に登場します——検索クエリは q=standing+desk%26price%3A200 として届き、 OAuth リダイレクトは next=https%3A%2F%2Fdashboard.internal%2F として、 ストレージパスは reports%2F2025%2Fq1.pdf として届きます。 JavaScript での URL デコードは、3 つの組み込み関数のうち正しいものを選ぶことに帰着します: decodeURIComponent() decodeURI()、および URLSearchParams——それらの間の選択が、 私が本番コードベースで見てきたほとんどのサイレントデータ破損の根本原因です。 特に + をスペースとして扱うエッジケースと 二重デコードの問題が顕著です。コードを書かずに素早く一度だけデコードするには、 ToolDeck の URL Decoder がブラウザで即座に処理します。この JavaScript URL デコードチュートリアルでは、すべての 3 つの 関数を詳しく解説します(ES2015+ / Node.js 10+):それぞれをいつ使うか、スペースと予約文字に おける違い、ファイルと HTTP リクエストからのデコード、安全なエラー処理、そして最も微妙な 本番バグを引き起こす 4 つの間違いについて説明します。

  • decodeURIComponent() はすべてのパーセントエンコードシーケンスをデコードします——個々のクエリパラメータ値とパスセグメントのデコードに適した選択肢です
  • decodeURI() は / ? & = # : などの構造的な URI 文字を保持します——完全な URL 文字列をデコードする場合にのみ使用し、個々の値には絶対に使用しないでください
  • URLSearchParams.get() はすでにデコードされた値を返します——その上で decodeURIComponent() を呼び出すと二重デコードが発生します
  • decodeURIComponent() は + をスペースとしてデコードしません——application/x-www-form-urlencoded 形式のデータには URLSearchParams を使用するか、デコード前に + を置換してください
  • 入力がユーザーデータから来る場合は、常に decodeURIComponent() を try/catch でラップしてください——裸の % や不完全なシーケンスは URIError をスローします

URL デコードとは?

パーセントエンコーディング(正式には RFC 3986 で定義されています)は、URL で安全でないか構造的に重要な文字を % 記号と 2 桁の 16 進数——その文字の UTF-8 バイト値——に置き換えます。URL デコードはこの変換を逆にします:各 %XX シーケンスは元のバイトに変換され、 結果のバイトシーケンスは UTF-8 テキストとして解釈されます。スペースは %20 から、スラッシュは %2F から、非 ASCII 文字 ü は %C3%BC(2 バイトの UTF-8 表現)からデコードされます。

エンコードされない文字は非予約文字と呼ばれます——文字 A–Z と a–z、数字 0–9、および - _ . ~。それ以外はすべて、URL 内で構造的な 役割を持つか(たとえば / はパスセグメントを区切り、 & はクエリパラメータを区切ります)、 データとして使用する場合にエンコードする必要があります。実際の結果として、 status=active&tier=premium のような 検索フィルターが単一のクエリパラメータ値としてエンコードされると、受け取り時に元の内容と まったく異なって見えます。

Before · text
After · text
// パーセントエンコード済み——HTTP リクエストまたは webhook ペイロードで受け取った形式
"q=price%3A%5B200+TO+800%5D%20AND%20brand%3ANorthwood%20%26%20status%3Ain-stock"
// URL デコード後——元の Elasticsearch クエリフィルター
"q=price:[200 TO 800] AND brand:Northwood & status:in-stock"

decodeURIComponent() — 値をデコードするための標準関数

decodeURIComponent() は JavaScript における URL デコードの主力関数です。入力文字列内のすべての %XX シーケンスをデコードします—— URL 内で構造的な意味を持つ文字も含みます。例えば %2F(スラッシュ)、 %3F(疑問符)、 %26(アンパサンド)、 %3D(等号)などです。これにより、個々のクエリ パラメータ値とパスセグメントのデコードに適した選択肢となりますが、完全な URL のデコードには 適しません——そこではこれらの構造的文字はエンコードされたままである必要があります。 グローバル関数であり、どの JavaScript 環境でもインポートは不要です。

最小限の動作例

JavaScript (browser / Node.js)
// 個々のクエリパラメータ値をデコード——よくあるケース

const city     = decodeURIComponent('S%C3%A3o%20Paulo')          // 'São Paulo'
const district = decodeURIComponent('It%C3%A1im%20Bibi')         // 'Itáim Bibi'
const category = decodeURIComponent('office%20furniture')         // 'office furniture'
const filter   = decodeURIComponent('price%3A%5B200+TO+800%5D')  // 'price:[200+TO+800]'
// 注意:+ はスペースとしてデコードされません——比較表の + セクションを参照

console.log(city)      // São Paulo
console.log(district)  // Itáim Bibi
console.log(category)  // office furniture
console.log(filter)    // price:[200+TO+800]

クエリパラメータから抽出したリダイレクト URL のデコード

JavaScript
// リダイレクト URL はパラメータ値として埋め込まれたときにパーセントエンコードされていた
// 受信側:抽出して使用する——URLSearchParams を使えば手動デコード不要

const incomingUrl = 'https://auth.company.com/callback' +
  '?next=https%3A%2F%2Fdashboard.internal%2Freports%3Fview%3Dweekly%26team%3Dplatform' +
  '&session_id=sid_7x9p2k'

const url       = new URL(incomingUrl)
const rawNext   = url.searchParams.get('next')       // URLSearchParams が自動デコード
const sessionId = url.searchParams.get('session_id') // 'sid_7x9p2k'

// rawNext はすでにデコードされています:'https://dashboard.internal/reports?view=weekly&team=platform'
// decodeURIComponent(rawNext) を再度呼び出さないこと——二重デコードになります

const nextUrl = new URL(rawNext!)
console.log(nextUrl.hostname)                    // dashboard.internal
console.log(nextUrl.searchParams.get('view'))    // weekly
console.log(nextUrl.searchParams.get('team'))    // platform

非 ASCII および Unicode パスセグメントのデコード

JavaScript
// 国際化されたパスセグメントを持つ REST API
// 元の文字の各 UTF-8 バイトが個別にパーセントエンコードされていた

const encodedSegments = [
  '%E6%9D%B1%E4%BA%AC',   // 東京 (Tokyo)    — 1 文字あたり 3 バイト
  '%E5%A4%A7%E9%98%AA',   // 大阪 (Osaka)    — 1 文字あたり 3 バイト
  'caf%C3%A9',            // café             — é は事前合成 NFC として
  'S%C3%A3o%20Paulo',     // São Paulo
]

encodedSegments.forEach(seg => {
  console.log(decodeURIComponent(seg))
})
// 東京
// 大阪
// café
// São Paulo

// ストレージ API URL からファイルキーを抽出
// オブジェクトキーに / が含まれていたため、パスセグメント内で %2F としてエンコードされた
const storageUrl = 'https://storage.api.example.com/v1/objects/reports%2F2025%2Fq1-financials.pdf'
const rawKey     = new URL(storageUrl).pathname.replace('/v1/objects/', '')
// .pathname は URL レベルのエンコーディングをデコードするが、%2F(URL レベルでは %252F)は残る
// 最後のステップで decodeURIComponent を使用:
const fileKey = decodeURIComponent(rawKey)  // 'reports/2025/q1-financials.pdf'
console.log(fileKey)
注意:decodeURIComponent() は入力に % の後に 2 つの有効な 16 進数字が続かない場合—— たとえば文字列末尾の裸の % %GH のようなシーケンス—— URIError をスローします。ユーザー提供の入力をデコードする際は、 常に try/catch でラップしてください。 安全なデコードパターンについては、下のエラー処理セクションで説明しています。

JavaScript URL デコード関数——文字リファレンス

3 つの組み込みデコード関数は、どのエンコードシーケンスをデコードするかが異なります。 以下の表は、実際に最も重要な文字の動作を示しています:

エンコード文字decodeURIComponent()decodeURI()URLSearchParams
%20スペーススペース ✅スペース ✅スペース ✅
++ (フォーム)+ (変換なし)+ (変換なし)スペース ✅
%2B+ リテラル+ ✅+ ✅+ ✅
%26&& ✅& (変換なし) ❌& ✅
%3D== ✅= (変換なし) ❌= ✅
%3F?? ✅? (変換なし) ❌? ✅
%23## ✅# (変換なし) ❌# ✅
%2F// ✅/ (変換なし) ❌/ ✅
%3A:: ✅: (変換なし) ❌: ✅
%40@@ ✅@ (変換なし) ❌@ ✅
%25%% ✅% ✅% ✅
%C3%BCüü ✅ü ✅ü ✅

重要な 2 行は + と構造的文字 (%26 %3D %3F) です。URLSearchParams は + をスペースとしてデコードします。 なぜなら application/x-www-form-urlencoded 仕様に従っているからです——HTML フォーム送信には正しいですが、同じ文字に対して decodeURIComponent() が行うことと異なります。 そして decodeURI() %26%3D%3F を静かにスキップします——値が実際に エンコードされたアンパサンドや等号を含むまでは正しく見えます。

decodeURI() — 構造を壊さずに完全な URL をデコード

decodeURI() encodeURI() の対となる関数です。 URI に構造的な意味を持つ文字を保持しながら完全な URL 文字列をデコードします: ; , / ? : @ & = + $ #。 これらはパーセントエンコードされた形式のまま残ります(または入力でエンコードされていない 文字として現れた場合はリテラル文字として)。これにより decodeURI() は、パスやホスト名に非 ASCII 文字を含む可能性のある完全な URL をサニタイズするのに安全です。クエリ文字列の構造を 誤って崩さずに済みます。

非 ASCII パスセグメントを持つ URL のサニタイズ

JavaScript
// 国際化されたパスセグメントと構造化クエリ文字列を持つ CDN URL
// decodeURI() は非 ASCII パスをデコードするが、? & = はそのまま保持

const encodedUrl =
  'https://cdn.example.com/assets/%E6%9D%B1%E4%BA%AC%2F2025%2Fq1-report.pdf' +
  '?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600'

const readable = decodeURI(encodedUrl)
console.log(readable)
// https://cdn.example.com/assets/東京/2025/q1-report.pdf?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600
// ↑ 非 ASCII がデコードされた;? & = は保持——URL は構造的に有効なまま

// decodeURIComponent は URL を破壊する——: / ? & = がすべて一度にデコードされる
const broken = decodeURIComponent(encodedUrl)
// ここでは同じように見えるが、'https%3A%2F%2F...' のような URL は破壊される
注意:個々の URL コンポーネントにもアクセスする必要がある場合は、 decodeURI() より URL コンストラクタを優先してください。new URL(str) は 入力を正規化し、構造を検証し、すでにデコードされたプロパティとして .pathname .searchParams .hostname を公開します。decodeURI() は、文字列結果のみが必要で、 URL コンストラクタを使用できない場合(たとえばグローバル URL クラスのない非常に古い Node.js 6 環境) のために残しておいてください。

URLSearchParams — クエリ文字列の自動デコード

URLSearchParams は、モダンな JavaScript で クエリ文字列を解析する慣用的な方法です。 .get() .getAll()、またはイテレーションによって 返されるすべての値は自動的にデコードされます——フォームエンコードされたデータの + をスペースとしてデコードすることも含みます。 すべてのモダンブラウザと Node.js 10+ でグローバルに利用でき、インポートは不要で、 手動の文字列分割が誤って処理するエッジケースも処理します。

受信クエリ文字列の解析

JavaScript (browser / Node.js 10+)
// webhook コールバックまたは OAuth リダイレクトクエリ文字列の解析
const rawSearch =
  '?event_id=evt_9c2f4a1b' +
  '&product_name=Standing+Desk+Pro' +
  '&filter=price%3A%5B200+TO+800%5D' +
  '&tag=ergonomic&tag=adjustable' +
  '&redirect=https%3A%2F%2Fdashboard.internal%2Forders%3Fview%3Dpending'

const params = new URLSearchParams(rawSearch)

console.log(params.get('event_id'))      // 'evt_9c2f4a1b'
console.log(params.get('product_name'))  // 'Standing Desk Pro'       ← + がスペースにデコード
console.log(params.get('filter'))        // 'price:[200+TO+800]'      ← %3A と %5B がデコード
console.log(params.getAll('tag'))        // ['ergonomic', 'adjustable']
console.log(params.get('redirect'))      // 'https://dashboard.internal/orders?view=pending'

// すべてのパラメータをイテレート
for (const [key, value] of params) {
  console.log(`${key}: ${value}`)
}
// event_id: evt_9c2f4a1b
// product_name: Standing Desk Pro
// filter: price:[200+TO+800]
// tag: ergonomic
// tag: adjustable
// redirect: https://dashboard.internal/orders?view=pending

現在のブラウザ URL からクエリパラメータを解析

JavaScript (browser)
// 現在の URL: https://app.example.com/search
//   ?q=standing+desk
//   &category=office+furniture
//   &sort=price_asc
//   &page=2

interface SearchFilters {
  query:    string | null
  category: string | null
  sort:     string
  page:     number
}

function getSearchFilters(): SearchFilters {
  const params = new URLSearchParams(window.location.search)
  return {
    query:    params.get('q'),                       // 'standing desk'
    category: params.get('category'),                // 'office furniture'
    sort:     params.get('sort') ?? 'relevance',
    page:     Number(params.get('page') ?? '1'),
  }
}

const filters = getSearchFilters()
console.log(filters.query)     // standing desk   (+ が自動デコード)
console.log(filters.category)  // office furniture

ファイルと API レスポンスからの URL エンコードデータのデコード

実際のプロジェクトでは 2 つのシナリオが常に発生します:パーセントエンコードされたデータを 含むディスク上のファイルの処理(アクセスログ、データエクスポート、webhook キャプチャファイル)と、 Node.js サーバーで受信 HTTP リクエストの URL を解析することです。どちらも同じ原則に従います ——手動の文字列分割ではなく、 URL コンストラクタまたは URLSearchParams を使用する——が、 詳細は異なります。

ファイルから URL エンコードされたレコードを読み取ってデコード

JavaScript (Node.js 10+)
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

// ファイル:orders-export.txt——1 行に 1 つの URL エンコードされたレコード
// customer_name=%E7%94%B0%E4%B8%AD%E5%A4%AA%E9%83%8E&order_id=ord_9c2f4a&product=Standing+Desk+Pro&total=14999%20JPY
// customer_name=%E5%B1%B1%E7%94%B0%E8%8A%B1%E5%AD%90&order_id=ord_7b3a1c&product=Ergonomic+Chair&total=8900%20JPY

interface Order {
  customerName: string | null
  orderId:      string | null
  product:      string | null
  total:        string | null
}

async function parseOrdersFile(filePath: string): Promise<Order[]> {
  const fileStream = createReadStream(filePath, { encoding: 'utf-8' })
  const rl         = createInterface({ input: fileStream, crlfDelay: Infinity })
  const orders: Order[] = []

  for await (const line of rl) {
    if (!line.trim()) continue

    // URLSearchParams は + をスペースとして %XX シーケンスを自動デコード
    const params = new URLSearchParams(line)

    orders.push({
      customerName: params.get('customer_name'),  // '田中太郎'
      orderId:      params.get('order_id'),        // 'ord_9c2f4a'
      product:      params.get('product'),         // 'Standing Desk Pro'
      total:        params.get('total'),           // '14999 JPY'
    })
  }

  return orders
}

const orders = await parseOrdersFile('./orders-export.txt')
console.log(orders[0])
// { customerName: '田中太郎', orderId: 'ord_9c2f4a', product: 'Standing Desk Pro', total: '14999 JPY' }

検索クエリをデコードするために Nginx アクセスログを解析

JavaScript (Node.js)
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

// Nginx アクセスログの行はこのように見えます:
// 192.168.1.42 - - [11/Mar/2026:10:23:01 +0000] "GET /api/search?q=standing%20desk%26brand%3ANorthwood&sort=price_asc HTTP/1.1" 200 1842

async function extractSearchQueries(logFile: string): Promise<string[]> {
  const rl     = createInterface({ input: createReadStream(logFile), crlfDelay: Infinity })
  const queries: string[] = []

  for await (const line of rl) {
    // ログ行からリクエストパスを抽出
    const match = line.match(/"GET ([^ ]+) HTTP/)
    if (!match) continue

    try {
      const requestUrl = new URL(match[1], 'http://localhost')
      const query      = requestUrl.searchParams.get('q')
      if (query) queries.push(query)  // URLSearchParams が自動デコード
    } catch {
      // 不正な形式の行をスキップ——アクセスログには切り捨てられたエントリが含まれる場合がある
    }
  }

  return queries
}

const queries = await extractSearchQueries('/var/log/nginx/access.log')
console.log(queries)
// ['standing desk&brand:Northwood', 'ergonomic chair', 'monitor arm 27 inch']

Node.js HTTP サーバーでクエリパラメータを解析

JavaScript (Node.js 10+)
import http from 'http'

// 受信 URL: /api/products?q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10&cursor=eyJpZCI6MTIzfQ%3D%3D

const server = http.createServer((req, res) => {
  // 完全な URL を構築——第 2 引数は URL コンストラクタが必要とするベース
  const requestUrl = new URL(req.url!, 'http://localhost')

  const searchQuery = requestUrl.searchParams.get('q')            // 'standing desk'
  const warehouseId = requestUrl.searchParams.get('warehouse')    // 'eu-west-1'
  const minStock    = Number(requestUrl.searchParams.get('minStock') ?? '0')
  const cursor      = requestUrl.searchParams.get('cursor')       // 'eyJpZCI6MTIzfQ=='

  if (!warehouseId) {
    res.writeHead(400, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: 'warehouse parameter is required' }))
    return
  }

  const cursorData = cursor ? JSON.parse(Buffer.from(cursor, 'base64').toString()) : null

  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ searchQuery, warehouseId, minStock, cursorData }))
})

server.listen(3000)

開発中にエンコードされた URL を検査する必要がある場合——解析コードを書く前に webhook が 送信している内容を理解するために——スクリプトを実行せずにデコードされた形式を即座に確認するには、 直接 ToolDeck の URL Decoder に貼り付けてください。

コマンドライン URL デコード

シェルスクリプト、CI パイプライン、またはエンコードされた文字列の素早い一度きりの検査のために、 完全なスクリプトを書かずに動作するいくつかのアプローチがあります。Node.js ワンライナーは クロスプラットフォームです;macOS と Linux では python3 も常に利用できます。

bash
# ── Node.js ワンライナー ──────────────────────────────────────────────────

# 単一のパーセントエンコードされた値をデコード
node -e "console.log(decodeURIComponent(process.argv[1]))" "S%C3%A3o%20Paulo%20%26%20Rio"
# São Paulo & Rio

# クエリ文字列を解析し、各キー=値ペア(デコード済み)を出力
node -e "
  const params = new URLSearchParams(process.argv[1])
  for (const [k, v] of params) console.log(`${k} = ${v}`)
" "q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10"
# q = standing desk
# warehouse = eu-west-1
# minStock = 10

# URL エンコードされた JSON ボディをデコードして美化表示(webhook デバッグで一般的)
node -e "
  const raw = decodeURIComponent(process.argv[1])
  console.log(JSON.stringify(JSON.parse(raw), null, 2))
" '%7B%22event%22%3A%22purchase%22%2C%22amount%22%3A14999%2C%22currency%22%3A%22JPY%22%7D'
# {
#   "event": "purchase",
#   "amount": 14999,
#   "currency": "JPY"
# }

# ── Python ワンライナー(ほとんどの macOS/Linux システムで利用可能)─────────
# unquote_plus も + をスペースとしてデコード——フォームエンコードデータに正しい
python3 -c "
from urllib.parse import unquote_plus
import sys
print(unquote_plus(sys.argv[1]))
" "Standing+Desk+%26+Ergonomic+Chair"
# Standing Desk & Ergonomic Chair

# ── curl——レスポンスヘッダーからリダイレクト URL をログに記録してデコード ──
curl -sI "https://api.example.com/short/abc123" | grep -i location |   node -e "
    const line = require('fs').readFileSync('/dev/stdin', 'utf8')
    const url  = line.replace(/^location:s*/i, '').trim()
    console.log(decodeURIComponent(url))
  "

decode-uri-component によるグレースフルデコード

組み込みの decodeURIComponent() は、 不正な形式のシーケンス——後ろに 2 桁の 16 進数字が続かない末尾の % を含む——に対して URIError をスローします。 ユーザー提供またはサードパーティの URL を処理する本番コード——検索クエリログ、 メールキャンペーンからのクリックスルー URL、スクレイピングされたウェブデータ——では、 不正な形式のシーケンスはクラッシュを引き起こすほど一般的です。 decode-uri-component パッケージ (週間 npm ダウンロード約 3,000 万回)は、スローする代わりに元のシーケンスをそのまま返すことで、 不正な形式のシーケンスをグレースフルに処理します。

bash
npm install decode-uri-component
# or
pnpm add decode-uri-component
JavaScript (Node.js)
import decodeUriComponent from 'decode-uri-component'

// ネイティブ関数は不正な入力でスロー——リクエストハンドラをクラッシュさせる可能性
try {
  decodeURIComponent('product%name%')   // ❌ URIError: URI malformed
} catch (e) {
  console.error('ネイティブがスローしました:', (e as Error).message)
}

// decode-uri-component はスローする代わりに元のシーケンスを返す
console.log(decodeUriComponent('product%name%'))
// product%name%   ← 不正な形式のシーケンスはそのまま、スローなし

// 有効なシーケンスは正しくデコードされる
console.log(decodeUriComponent('S%C3%A3o%20Paulo%20%26%20Rio'))
// São Paulo & Rio

// 混合:有効なシーケンスはデコード、無効なものは保持
console.log(decodeUriComponent('Tokyo%20Office%20%ZZ%20HQ'))
// Tokyo Office %ZZ HQ   ← %ZZ は有効な 16 進数ではない——そのまま保持

// ログ処理パイプラインでの実際の使用
const rawSearchQueries = [
  'standing%20desk%20ergonomic',
  'price%3A%5B200+TO+800%5D',
  '50%25+off',       // ← decodeURIComponent でスロー(裸の %)
  'wireless%20keyboard%20%26%20mouse',
]

const decoded = rawSearchQueries.map(q => decodeUriComponent(q))
console.log(decoded)
// ['standing desk ergonomic', 'price:[200+TO+800]', '50%25+off', 'wireless keyboard & mouse']

予期しない入力が到着した場合に即座に知りたいアプリケーションコードでは、 try/catch 付きの decodeURIComponent() を使用してください。 一部の入力に無効なエンコーディングが含まれていても処理を続けたいデータパイプライン、 ログプロセッサー、webhook ハンドラーでは decode-uri-component を使用してください。

不正な形式のパーセントエンコード文字列の処理

裸の %、不完全なシーケンス %A、または無効なペア %GH はすべて、 decodeURIComponent() URIError: URI malformed をスローさせます。 ユーザー制御の入力——検索クエリ、URL フラグメント、URL を含むフォームフィールド、 メールキャンペーンリンクのパラメータ——はこれらのシーケンスを含む可能性があります。 外部に面したコードには安全なラッパーが不可欠です。

一般的なシナリオ向けの安全なデコードラッパー

JavaScript
// ── 1. 基本的な安全デコード——エラー時に元の文字列を返す ─────────────
function safeDecode(encoded: string): string {
  try {
    return decodeURIComponent(encoded)
  } catch {
    return encoded  // デコード失敗時は生の入力を返す——絶対にクラッシュしない
  }
}

// ── 2. 安全デコード + + をスペースとして処理(フォームエンコード値向け)────
function safeFormDecode(formEncoded: string): string {
  try {
    return decodeURIComponent(formEncoded.replace(/+/g, ' '))
  } catch {
    return formEncoded.replace(/+/g, ' ')  // 残りが失敗しても少なくとも + を置換
  }
}

// ── 3. 安全な完全クエリ文字列パーサー→プレーンオブジェクト ──────────────
function parseQueryString(queryString: string): Record<string, string> {
  const result: Record<string, string> = {}
  try {
    const params = new URLSearchParams(queryString)
    for (const [key, value] of params) {
      result[key] = value  // URLSearchParams が内部ですべてのデコードを処理
    }
  } catch {
    // 完全に不正な入力の場合は空を返す
  }
  return result
}

// 使用例
console.log(safeDecode('S%C3%A3o%20Paulo'))         // São Paulo
console.log(safeDecode('search%20for%2050%25+off'))  // search for 50% off(裸の %)
                                                     // → 実際には大丈夫;% はここで %25
console.log(safeDecode('malformed%string%'))         // malformed%string%(スローなし)

console.log(safeFormDecode('standing+desk+pro'))     // standing desk pro

console.log(parseQueryString('q=hello+world&tag=node%20js&page=2'))
// { q: 'hello world', tag: 'node js', page: '2' }

よくある間違い

本番環境でサイレントデータ破損または予期しないクラッシュを引き起こすこれらの 4 つのパターンを 繰り返し見てきました——通常、値がたまたま特殊文字を含む場合にのみ表面化します。 つまりユニットテストをすり抜けて、実際のユーザーデータでのみ現れるわけです。

間違い 1 — URLSearchParams の値を二重デコード

問題: URLSearchParams.get() はすでにデコードされた 文字列を返します。その上で decodeURIComponent() を呼び出すと値が 二重デコードされ——デコードされた出力に残っている % %25 に変えてデータを破損させます。 修正: URLSearchParams.get() からの値を直接使用 してください——追加のデコードは不要です。

Before · JavaScript
After · JavaScript
// ❌ URLSearchParams はすでにデコード済み——再デコードすると % を含む値が破損する
const qs        = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rawRate   = qs.get('rate')       // '50%'   ← すでにデコード済み
const wrongRate = decodeURIComponent(rawRate)
// '50%25'  ← % が 2 回目のパスで %25 にデコードされた——また間違った結果に
// ✅ URLSearchParams.get() を直接使用——デコードは自動
const qs       = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rate     = qs.get('rate')        // '50%'   ← 正しい、追加のステップ不要
const redirect = qs.get('redirect')    // 'https://dashboard.internal/'
const parsed   = new URL(redirect!)    // 構築しても安全——すでにデコード済み
console.log(parsed.hostname)           // dashboard.internal

間違い 2 — フォームエンコードデータの + をスペースとしてデコードしない

問題: フォーム送信と一部の OAuth ライブラリはスペースを + (application/x-www-form-urlencoded) としてエンコードします。このデータに decodeURIComponent() を呼び出すと + はリテラルのプラス記号のままになります。 修正: フォームエンコードデータの解析には URLSearchParams を使用するか、decodeURIComponent() を呼び出す前に + をスペースに置換してください。

Before · JavaScript
After · JavaScript
// ❌ decodeURIComponent は + をスペースではなくリテラルのプラスとして扱う
// フォームボディ:customer_name=田中太郎&product=Standing+Desk+Pro&qty=2
const formBody   = 'customer_name=%E7%94%B0%E4%B8%AD%E5%A4%AA%E9%83%8E&product=Standing+Desk+Pro&qty=2'
const [, rawVal] = formBody.split('&')[0].split('=')
const name       = decodeURIComponent(rawVal)
console.log(name)  // '田中太郎'  ← ここは OK だが + はスペースに変換されない
// ✅ URLSearchParams を使用——application/x-www-form-urlencoded 仕様に従う
const formBody   = 'customer_name=%E7%94%B0%E4%B8%AD%E5%A4%AA%E9%83%8E&product=Standing+Desk+Pro&qty=2'
const params     = new URLSearchParams(formBody)
console.log(params.get('customer_name'))  // '田中太郎'   ← 正しくデコード
console.log(params.get('product'))        // 'Standing Desk Pro'  ← + が正しくスペースにデコード

間違い 3 — 個々のクエリパラメータ値に decodeURI() を使用

問題: decodeURI() %26 (&)、 %3D (=)、または %3F (?) をデコードしません—— パラメータ値の内部でよくエンコードされる文字です。単一の値のデコードに使用すると これらのシーケンスはそのまま残り、静かに誤った出力を生成します。 修正: 個々の値には decodeURIComponent() を使用し、decodeURI() は完全な URL 文字列のために 残しておいてください。

Before · JavaScript
After · JavaScript
// ❌ decodeURI は & と = をデコードしない——値は壊れたまま
const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'
const filter        = decodeURI(encodedFilter)
console.log(filter)
// 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'  ← %3D と %26 がデコードされない
// ✅ decodeURIComponent は & と = を含むすべてのシーケンスをデコード
const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'
const filter        = decodeURIComponent(encodedFilter)
console.log(filter)
// 'status=active&tier=premium&region=eu-west'  ← 正しくデコードされた

間違い 4 — ユーザー入力の decodeURIComponent に try/catch をラップしない

問題: 2 桁の 16 進数字が後ろに続かない裸の %——ユーザーが入力した検索クエリで よく見られる(「50% オフ」、「100% 綿」)——が decodeURIComponent() URIError: URI malformed をスローさせ、 キャッチされなければリクエストハンドラーをクラッシュさせます。 修正: 入力がユーザーデータ、URL フラグメント、 または外部システムから来る場合は、常に try/catch でラップしてください。

Before · JavaScript
After · JavaScript
// ❌ ユーザーが検索ボックスに '100% 綿' を入力——裸の % でサーバーがクラッシュ
// GET /api/search?q=100%25+綿  ← この具体的なケースは OK (%25 = %)
// GET /api/search?q=100%+綿    ← これはクラッシュ(% の後に 2 桁の hex がない)
app.get('/api/search', (req, res) => {
  const query = decodeURIComponent(req.query.q as string)
  // ↑ '100% 綿' に対して URIError: URI malformed をスロー → 未処理の 500 エラー
})
// ✅ try/catch でラップ——デコード失敗時は生の入力にフォールバック
app.get('/api/search', (req, res) => {
  let query: string
  try {
    query = decodeURIComponent(req.query.q as string)
  } catch {
    query = req.query.q as string  // クラッシュする代わりに生の値を使用
  }
  // 安全に処理を続行——query はデコードされているか生の値のどちらか
})

decodeURIComponent vs decodeURI vs URLSearchParams——クイック比較

メソッド%XX をデコード+ をスペースにデコード不正形式でスロー& = ? # をデコードユースケースインストール
decodeURIComponent()✅ 全て❌ いいえ✅ URIError✅ はい個々の値とパスセグメントNo
decodeURI()✅ ほとんど❌ いいえ✅ URIError❌ いいえ完全な URL 文字列No
URLSearchParams✅ 全て✅ はい❌ 静か✅ はい自動デコード付きクエリ文字列解析No
URL constructor✅ 全て✅ はい✅ TypeError✅ はい完全な URL 解析と正規化No
decode-uri-component✅ 全て❌ いいえ❌ 静か✅ はい不正入力耐性のバッチデコードnpm install
querystring.unescape()✅ 全て❌ いいえ❌ 静か✅ はいレガシー Node.js (v16 で非推奨)No (built-in)

ほとんどの場合、選択は 3 つのシナリオに絞られます。クエリ文字列の解析には URLSearchParams を使用してください—— デコード、+ をスペースとして扱うルール、 および重複キーを自動的に処理します。単一の値またはパスセグメントには( try/catch でラップした) decodeURIComponent() を使用してください。 特にセグメント内に %2F エンコードされたスラッシュが 含まれることが予想される場合はそうです。完全な URL 文字列のパスに非 ASCII 文字があり、 構造的文字 (/ ? & =) を出力でエンコードされた ままにする必要がある場合のみ、 decodeURI() を使用してください。

よくある質問

JavaScript における decodeURIComponent() と decodeURI() の違いは何ですか?
decodeURIComponent() は文字列内のすべてのパーセントエンコードシーケンスをデコードします。URL 内で構造的な意味を持つ文字も含みます——& (%26)、= (%3D)、? (%3F)、# (%23)、/ (%2F)、: (%3A) など。個々のクエリパラメータ値またはパスセグメントのデコードに設計されています。decodeURI() はこれらの構造的文字を保持します——%26、%3D、%3F、%23、%2F、%3A はデコードしません——なぜならクエリ文字列の構造を壊さずに完全な URL をサニタイズするために設計されているからです。エンコードされた & または = を含む単一のパラメータ値に decodeURI() を使用すると、これらのシーケンスはデコードされないまま静かに誤った出力を生成します。
decodeURIComponent() が一部の文字列で URIError をスローするのはなぜですか?
入力に % の後に正確に 2 桁の有効な 16 進数字が続かない場合、decodeURIComponent() は URIError: URI malformed をスローします。よくあるトリガー:文字列末尾の裸の %(ユーザーが入力した「50% オフ」)、不完全なシーケンス("%A")、または非 16 進ペア("%GH")。これは URL エンコードされることを意図していなかったテキストからコピーされた値や、ユーザーが入力した検索クエリで最も頻繁に発生します。修正方法は decodeURIComponent() を try/catch ブロックでラップし、エラー時に生の文字列を返すことです。decode-uri-component npm パッケージは try/catch ラッパーを必要とせずに同じ修正を提供します。
URLSearchParams はパーセントエンコードされた値を自動的にデコードしますか?
はい。URLSearchParams.get() と URLSearchParams.getAll() によって返されるすべての値は完全にデコードされています——その出力に decodeURIComponent() を呼び出すべきではありません。URLSearchParams はまた application/x-www-form-urlencoded 仕様に従っており、+ をスペースとしてデコードします。これにより HTML フォーム送信と OAuth トークンレスポンスに対して正しい動作をします。URLSearchParams が役立たない唯一のケースは、クエリ文字列の一部ではないスタンドアロンのエンコード値をデコードする場合です——その場合は try/catch 付きの decodeURIComponent() を使用してください。
JavaScript で + 記号をスペースとしてデコードするにはどうすればいいですか?
decodeURIComponent() は + をリテラルのプラス記号として扱います——+ をスペースに変換しません。これは意図的なものです:+ をスペースとして扱うのは application/x-www-form-urlencoded の慣習であり、パーセントエンコーディング標準とは別のものです。+ をスペースとしてデコードするには、URLSearchParams(フォームエンコード仕様に従う)を使用するか、decodeURIComponent() を呼び出す前に + を置換してください:decodeURIComponent(str.replace(/\+/g, ' '))。+ を %20 に置き換えてからデコードする方法も機能しますが少し冗長です。完全なクエリ文字列を解析する場合は常に URLSearchParams を優先してください——%20 と + の両方を正しく処理します。
Node.js で文字列を URL デコードするにはどうすればいいですか?
すべてのブラウザで利用できるのと同じグローバル関数を使用してください——Node.js 10+ ではインポートは不要です。個々の値には decodeURIComponent()、完全な URL 文字列には decodeURI()、クエリ文字列には URLSearchParams を使用します。受信 HTTP リクエスト URL には new URL(req.url, 'http://localhost') を使用してください——URL コンストラクタは URL を正規化し、.searchParams(自動デコード)と .pathname(URL レベルでデコード)を公開します。古い querystring.unescape() 関数はまだ存在しますが、Node.js 16 から非推奨です——代わりに decodeURIComponent() を優先してください。
JavaScript で URL クエリ文字列をオブジェクトに解析するにはどうすればいいですか?
Object.fromEntries() と URLSearchParams を使用してください:const params = Object.fromEntries(new URLSearchParams(queryString))。これにより、すべてのパラメータキーとデコードされた値を持つプレーンオブジェクトが得られます。Object.fromEntries() は重複キーの最後の値のみを保持することに注意してください——tag=one&tag=two のような重複キーには、代わりに URLSearchParams.getAll('tag') を使用してください。ブラウザでは、new URLSearchParams(window.location.search) が現在のページのクエリ文字列を自動的に解析します。Node.js では new URL(req.url, base).searchParams から解析してください。

関連ツール

コードを書かずにワンクリックでデコードするには、パーセントエンコードされた文字列を直接 ToolDeck の URL Decoder に貼り付けてください——ブラウザで即座にデコードし、結果はコードやターミナルにコピーする 準備が整っています。

AC
Alex ChenFront-end & Node.js Developer

Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.

MW
Marcus Webb技術レビュアー

Marcus specialises in JavaScript performance, build tooling, and the inner workings of the V8 engine. He has spent years profiling and optimising React applications, working on bundler configurations, and squeezing every millisecond out of critical rendering paths. He writes about Core Web Vitals, JavaScript memory management, and the tools developers reach for when performance really matters.