From the EDN specs page:

edn supports a rich set of built-in elements, and the definition of extension elements in terms of the others. Users of data formats without such facilities must rely on either convention or context to convey elements not included in the base set. This greatly complicates application logic, betraying the apparent simplicity of the format. edn is simple, yet powerful enough to meet the demands of applications without convention or complex context-sensitive logic.


In haskell this "rich set of built-in elements" is represented with two types.

A container:

data Value
  = Nil
  | Boolean   Bool
  | String    Text
  | Character Char
  | Symbol    Text Text
  | Keyword   Text
  | Integer   Int
  | Floating  Double
  | List      EDNList
  | Vec       EDNVec
  | Map       EDNMap
  | Set       EDNSet

And a generic wrapper for tagging things:

data Tagged tag a
  = Tagged tag tag a
  | NoTag a

Then, they are combined in a resulting container:

type TaggedValue = Tagged Text Value


To convert between text representation and AST there are two aptly named functions (collected in Data.EDN):

parseText :: Monad m => String -> Text -> m TaggedValue
renderText :: TaggedValue -> Text
pretty $ parseText @Maybe "example" "#fancy/s-expressions (are [my/bread and butter])" => 
        [ NoTag (Symbol "" "are")
        , NoTag
               [ NoTag (Symbol "my" "bread")
               , NoTag (Symbol "" "and")
               , NoTag (Symbol "" "butter")
renderText $ NoTag (Symbol "my" "bread") => 

Note: parseText expects its input as a signe-expression document. You need to wrap it in your favourite container to have "multiple declarations".

While pattern-matching on this simple if "rich" set of constructors is fun, there is a way to shuttle values to and from EDN documents.