Your guesses about how to plan your game look good and give you room for further improvement too. Prefabs make GameObject management easier, and let you edit once and apply changes everywhere in your project. The JSON format is easy to understand and debug, and enough for your starting builds; it isn't either good or bad, it just depends on what you need to do.
This means all answers here are going to be subjective and depend on others' thinking and approach to coding, then you may want to be more specific about what you need to solve.
My two pennies worth:
Scriptable objects are a good way to set up instances sharing the same codebase but behaving differently. You can easily change public variables through the inspector, then creating the same prefab with different starting values. This approach can be combined with prefab variants so that Unity recognises these as the same GameObject type (even if you have 10 different variants).
Card info can be stored as JSON, plain text, a dictionary... Any solution is suitable as long as it works - until you want to optimise and improve your workflow. You'd want to store unique information only, such as cards name, cost, description; whereas features like spawn, mulligan, and special effects are scripted and can be referenced with simple labels (different cards may have the same effect, even with different parameters), adding/removing them at runtime.
EDIT: game state and animations
Additionally, I suppose your game needs animations (e.g. drawing cards, giving visual feedback on attacks/effects...) that you need to separate from the game logic:
- The internal game state should only step forward based on individual players' actions and in-game chained effects;
- Animations should purely and simply animate stuff.
This means that the animation component should never tell the game logic to step forward so that it can display the next animation. It's the other way round: the game logic tells the animation component that something happened and that a certain animation must be played; once the animation has finished, the logic can move forward and update the game state once again, triggering a new animation, and so on.
How to do that? Unity features Coroutines, which you can treat like functions that give you control over their execution time rather than returning right after completion. Therefore, you can tell a coroutine to do something and wait for a certain amount of seconds before moving on. And, you can also nest coroutines:
Coroutine1
starts;
Coroutine1
calls Coroutine2
and pauses execution;
Coroutine2
starts;
Coroutine2
does something;
Coroutine2
returns;
Coroutine1
resumes execution.
You can now replace Coroutine1
with your game logic and Coroutine2
with any routine animating cards and effects in your current game. All animations will end before moving on with the game, BUT it's the game logic that waits for animation completion and not the animation itself to step the game state forward.