If your custom controls work in Editor but reset to defaults on Android, the binding override JSON is usually being read from the wrong path, loaded before the file is available, or blocked by legacy storage assumptions.
This fix path gives you a deterministic Android-safe flow: save and load binding overrides from Application.persistentDataPath, migrate old files once, and only apply overrides after your InputActionAsset is initialized.
Problem summary
Common symptoms:
- Rebinds are successful during gameplay, but restart returns to default controls.
- Android logs show file-not-found or empty JSON when loading overrides.
- Rebinding works on Windows or in Editor but fails on APK or AAB builds.
- Controls reset after app update, package rename, or save-data migration.
Impact:
- accessibility and player preference settings are lost
- QA cannot verify control persistence across builds
- release confidence drops for mobile input reliability
Root causes
-
Wrong save/load location on Android Saving to temporary or editor-only paths instead of
Application.persistentDataPath. -
Scoped storage mismatch Legacy external-storage assumptions break on newer Android versions.
-
Load-order race Applying overrides before actions/maps are enabled or before JSON read completes.
-
No migration for legacy filename or folder changes Existing users keep old JSON in a previous location after app updates.
-
Silent parse failure Invalid or truncated JSON is read without validation, so defaults stay active.
Step-by-step fix
Step 1 - Standardize one persistent path contract
Use one canonical file path:
- define
rebindFilePath = Path.Combine(Application.persistentDataPath, "input_rebinds.json") - use the same path for both save and load
- log the resolved path in development builds for diagnostics
Do not use StreamingAssets, temporaryCachePath, or editor-only relative paths for user-authored rebind data.
Step 2 - Save and load only Input System binding overrides
For Unity Input System:
- save with
InputActionAsset.SaveBindingOverridesAsJson() - load with
InputActionAsset.LoadBindingOverridesFromJson(jsonString) - keep this data separate from unrelated profile JSON
This avoids schema drift when non-input profile data changes.
Step 3 - Add one-time legacy migration on startup
Before loading active overrides:
- check old known locations or filenames used by prior builds
- if legacy file exists and new file does not, copy legacy file to canonical path
- write one migration marker so copy runs only once
Run migration before any call to LoadBindingOverridesFromJson.
Step 4 - Validate file presence and JSON integrity before apply
Safe load sequence:
File.Exists(rebindFilePath)guardFile.ReadAllTextwith exception handling- reject empty or whitespace-only payload
- parse/apply overrides inside guarded block
- on failure, keep defaults and log reason with file size and timestamp
Failing closed with explicit logs is better than silently applying partial state.
Step 5 - Apply overrides after input actions are ready
Load timing should occur after action asset initialization:
- initialize your input singleton or player input wrapper
- ensure action maps are available
- apply loaded overrides
- enable gameplay maps
If you apply too early in bootstrap, overrides may appear to load but be overwritten by later initialization.
Verification checklist
- [ ] Rebind on Android device, restart app, and confirm controls persist.
- [ ] Upgrade from previous build and verify legacy migration preserves existing rebinds.
- [ ] Log confirms canonical
persistentDataPathand successful JSON apply. - [ ] Corrupt JSON test falls back to defaults with explicit warning logs.
- [ ] No editor-only or external-storage path is required for runtime persistence.
Alternative fixes for edge cases
- If rebinds are profile-specific, suffix filename by user slot (for example
input_rebinds_slot_2.json) while keeping the same canonical directory. - If cloud sync is enabled, merge local file first, then apply cloud patch to avoid wiping recent offline rebinds.
- If multiple
InputActionAssetinstances exist, centralize save/load in one owner to avoid last-write conflicts.
Prevention tips
- Keep one versioned wrapper for rebind JSON so future format changes are migration-safe.
- Add Android smoke test in CI or QA checklist: rebind -> force close -> relaunch -> verify.
- Record rebind load outcome metrics (success, missing file, parse fail, migration used) in release telemetry.
- Avoid changing filename/path conventions during late release windows without migration logic.
FAQ
Why does rebinding work in Editor but not on Android build
Editor often reads from different writable locations and has fewer storage restrictions. Android runtime requires explicit persistent path handling and stable startup load order.
Should we request storage permissions for rebinding JSON
Usually no. For app-internal persistence, Application.persistentDataPath is the correct approach and does not require broad external storage permissions.
Can we store rebinding data in PlayerPrefs instead
You can for small payloads, but JSON file persistence is easier to migrate, inspect, and validate across versions and profiles.
Related links
- Unity Cloud Save Conflict Resolution Overwrites Newer Data - Last-Write and Merge Strategy Fix
- Unity Cloud Diagnostics Symbol Upload Failed - dSYM and ProGuard Mapping Pipeline Fix
- Steamworks Callbacks Not Firing in Unity Editor - Run in Background and Pump Timing Fix
- Steamworks Overlay Missing in Release Build - Redistributable and Launch Context Fix
- Official docs: Input System manual and Application.persistentDataPath
Bookmark this fix before your next Android submission pass, and share it with your gameplay and QA teammates if it helps stabilize control persistence.