diff --git a/src/main/scala/spray/json/SnakeCaseJsonSupport.scala b/src/main/scala/spray/json/SnakeCaseJsonSupport.scala new file mode 100644 index 00000000..19aecbcb --- /dev/null +++ b/src/main/scala/spray/json/SnakeCaseJsonSupport.scala @@ -0,0 +1,51 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2013-2015 Andrew Snare, Age Mooij + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package spray.json + +/** + * A custom version of the Spray DefaultJsonProtocol with a modified field naming strategy + */ +trait SnakeCaseJsonSupport extends DefaultJsonProtocol { + import reflect._ + + /** + * This is the most important piece of code in this object! + * It overrides the default naming scheme used by spray-json and replaces it with a scheme that turns camelcased + * names into snakified names (i.e. using underscores as word separators). + */ + override protected def extractFieldNames(classTag: ClassTag[_]) = { + import java.util.Locale + + def snakify(name: String) = PASS2.replaceAllIn(PASS1.replaceAllIn(name, REPLACEMENT), REPLACEMENT).toLowerCase(Locale.US) + + super.extractFieldNames(classTag).map { snakify(_) } + } + + private val PASS1 = """([A-Z]+)([A-Z][a-z])""".r + private val PASS2 = """([a-z\d])([A-Z])""".r + private val REPLACEMENT = "$1_$2" +} + +object SnakeCaseJsonSupport extends SnakeCaseJsonSupport + diff --git a/src/test/scala/spray/json/SnakeCaseJsonSupportSpec.scala b/src/test/scala/spray/json/SnakeCaseJsonSupportSpec.scala new file mode 100644 index 00000000..964f6e7a --- /dev/null +++ b/src/test/scala/spray/json/SnakeCaseJsonSupportSpec.scala @@ -0,0 +1,105 @@ +package spray.json + +import org.specs2.mutable._ + +class SnakeCaseJsonSupportSpec extends Specification with SnakeCaseJsonSupport { + + "SnakeCaseJsonSupport" should { + "convert field names to snake cased fields names." in { + // Given + case class SampleFields( + HelloWorld: Int = 0, + A1AAbBBBBBbCCcc: Int = 0, + AbAAbbAAAbbb1AA2bb1AA2AAAAbb1bAA: Int = 0, + x: Int = 0, + Y: Int = 0, + `^There-arE_Non_(Alphabetic))_character!S`: Int = 0, + `tHere-Are_(AlsO)_noN_(alphaBetic))_CharaCter!s`: Int = 0, + ` There are space character `: Int = 0, + ` これは マルチバイトAbcのaBc確認です。`: Int = 0, + ab: Int = 0, + AC: Int = 0, + Ad: Int = 0, + bA: Int = 0, + AAA: Int = 0, + AAb: Int = 0, + AbA: Int = 0, + bAA: Int = 0, + bbA: Int = 0, + bAb: Int = 0, + Acc: Int = 0, + ccc: Int = 0 + ) + case class SampleFields2( + aaa123bbb: Int = 0, + aaA123bbb: Int = 0, + aaa123Bbb: Int = 0, + aaA123Bbb: Int = 0, + aaa123: Int = 0, + aaA123: Int = 0, + aAA123: Int = 0 + ) + implicit val jsonFormat1: RootJsonFormat[SampleFields] = + jsonFormat21(SampleFields.apply) + implicit val jsonFormat2: RootJsonFormat[SampleFields2] = + jsonFormat7(SampleFields2.apply) + // When + val sampleFields1 = SampleFields().toJson.asJsObject + val sampleFields2 = SampleFields2().toJson.asJsObject + // Then + val fields1 = sampleFields1.fields + fields1 must haveKey ("hello_world") + fields1 must haveKey ("a1_a_ab_bbbb_bb_c_ccc") + fields1 must haveKey ("ab_a_abb_aa_abbb1_aa2bb1_aa2_aaa_abb1b_aa") + fields1 must haveKey ("x") + fields1 must haveKey ("y") + fields1 must haveKey ("^there-ar_e_non_(alphabetic))_character!s") + fields1 must haveKey ("t_here-are_(als_o)_no_n_(alpha_betic))_chara_cter!s") + fields1 must haveKey (" there are space character ") + fields1 must haveKey (" これは マルチバイトabcのa_bc確認です。") + fields1 must haveKey ("ab") + fields1 must haveKey ("ac") + fields1 must haveKey ("ad") + fields1 must haveKey ("b_a") + fields1 must haveKey ("aaa") + fields1 must haveKey ("a_ab") + fields1 must haveKey ("ab_a") + fields1 must haveKey ("b_aa") + fields1 must haveKey ("bb_a") + fields1 must haveKey ("b_ab") + fields1 must haveKey ("acc") + fields1 must haveKey ("ccc") + val fields2 = sampleFields2.fields + fields2 must haveKey("aaa123bbb") + fields2 must haveKey("aa_a123bbb") + fields2 must haveKey("aaa123_bbb") + fields2 must haveKey("aa_a123_bbb") + fields2 must haveKey("aaa123") + fields2 must haveKey("aa_a123") + fields2 must haveKey("a_aa123") + } + "deserialize snake cased json" in { + // Given + case class SampleFields( + a: Int, + aA: Int, + helloWorld: Int, + HelloTheWorld: Int + ) + implicit val jsonFormat: RootJsonFormat[SampleFields] = + jsonFormat4(SampleFields.apply) + val json = JsObject( + "hello_the_world" -> 4.toJson, + "a_a" -> 2.toJson, + "a" -> 1.toJson, + "hello_world" -> 3.toJson + ) + // When + val result = jsonFormat.read(json) + // Then + result mustEqual SampleFields( + a = 1, aA = 2, helloWorld = 3, HelloTheWorld = 4 + ) + } + } +}