Yes. In fact, quite easy.
implicit lazy val userWrites: Writes[User] = new Writes[User] {
def writes(user: User) = Json.obj(
"name" -> user.name,
"friends" -> user.friends
)
}
val joe = User("Joe", Nil)
val bob = User("Bob", Nil)
val jane = User("Jane", Seq(bob, joe))
val james = User("James", Seq(bob, jane))
scala> Json.toJson(james)
res0: play.api.libs.json.JsValue = {"name":"James","friends":[{"name":"Bob","friends":[]},{"name":"Jane","friends":[{"name":"Bob","friends":[]},{"name":"Joe","friends":[]}]}]}
userWrites doesn't really need to be lazy either. It works just fine like this because the combinators try to generate the writes by resolving child writes implicitly and working it's way down the tree, which is why it needs the lazyWrite to stop it from descending infinitely in a recursive structure. When writing def writes, we're explicitly stating what the writes are.
Neither, however, can save you from this scenario:
def jim: User = User("Joe", Seq(dwight))
def dwight: User = User("Bob", Seq(jim))