object [ "foo" .= object [ "bar" .= [ "Yum", "Waffles!"] ] ]
Which will give you {"foo": {"bar": ["Yum", "Waffles!"]}}.
You can do traversals when decoding too:
tasty :: JSON -> Parser [String] tasty json = json .: "foo" >>= (.: "bar")
You can also do similar things with lens-aeson:
tasty json = json & key "foo" . key "bar" . _Array
Though I'm less familiar with that, and you won't be inside the Parser monad.
For example, rather than having an Invoice with a status field of Text or String, I have a type InvoiceStatus = Paid | Unpaid | Void. From there, you can write a very precise parser that takes exactly what you want and nothing else. When I apply this approach to each component of the thing I need to parse (or do it generically even), I don't really find myself having to munge javascript types into my data structures.
That said, aeson support for nullary types (i.e. types that hold no "extra" values, like InvoiceStatus above) is trivial and the types make a huge difference in the rest of your code.
So, complex nested structures are a matter of defining Show and Generic instances for any types that don't already have them. From there you can trivially make those types aeson-compatible. Since so many common types are already supported, adding support for your own nested types isn't too difficult. The example use automatic derivation of Generic instances, which may result in some performance problems but is the easiest way to get off the ground.