Started migrating item/ability/enemy data off JSON a few months ago and haven't looked back.
Basic pattern: extend Resource, export your fields, save as .tres:
class_name ItemData
extends Resource
@export var item_name: String = ""
@export var base_damage: float = 0.0
@export var stack_limit: int = 1
@export var icon: Texture2D
@export_group("Audio")
@export var pickup_sound: AudioStream
@export var use_sound: AudioStream
What you get: type safety, editor-native editing, nested resources, autocompletion everywhere. The thing that surprised me most was how well nested resources work. Ability data used to be JSON with string IDs I resolved manually at runtime. Now it's just typed references, and Godot handles loading the whole graph.
@export_group and @export_subgroup also help a lot for organization. Big flat configs turn into something you can actually navigate in the inspector without squinting.
Downsides I've hit: .tres diffs in git are ugly (they store internal UIDs), and if you need to export data to non-Godot tools, like a balance spreadsheet or a web dashboard, you're writing a converter. Also, watch for circular resource references. Godot handles them, but it's a footgun if you're not expecting it.
Curious if anyone's done this at scale. Do large resource graphs cause noticeable load times? And is anyone doing resource inheritance, like extending a base WeaponData for swords vs axes, or just keeping everything flat?