diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4fd175..03de709 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]
### Added
+
+- `StringId, Comparable`: Base types for Strongly Typed Ids with string renditions [#116](https://github.com/jet/FsCodec/pull/116)
+- `NewtonsoftJson.StringIdConverter`: Converter for `StringId` [#116](https://github.com/jet/FsCodec/pull/116)
+- `SystemTextJson.StringIdConverter`: Converter for `StringId` [#116](https://github.com/jet/FsCodec/pull/116)
+- `SystemTextJson.StringIdOrDictionaryKeyConverter`: Converter for `StringId` that enables `Dictionary` values using a `StringId`-derived type as a key to be used as a JSON Object Key [#116](https://github.com/jet/FsCodec/pull/116)
+
### Changed
### Removed
### Fixed
diff --git a/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj b/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj
index b8a5ee3..3521c24 100644
--- a/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj
+++ b/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj
@@ -13,6 +13,7 @@
+
diff --git a/src/FsCodec.NewtonsoftJson/StringIdConverter.fs b/src/FsCodec.NewtonsoftJson/StringIdConverter.fs
new file mode 100644
index 0000000..f2b6261
--- /dev/null
+++ b/src/FsCodec.NewtonsoftJson/StringIdConverter.fs
@@ -0,0 +1,8 @@
+namespace FsCodec.NewtonsoftJson
+
+/// Implements conversion to/from string for a FsCodec.StringId-derived type.
+[]
+type StringIdConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
+ inherit JsonIsomorphism<'T, string>()
+ override _.Pickle value = value.ToString()
+ override _.UnPickle input = parse input
diff --git a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
index e221a36..420f367 100644
--- a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
+++ b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
@@ -15,6 +15,7 @@
+
diff --git a/src/FsCodec.SystemTextJson/StringIdConverter.fs b/src/FsCodec.SystemTextJson/StringIdConverter.fs
new file mode 100644
index 0000000..07e489e
--- /dev/null
+++ b/src/FsCodec.SystemTextJson/StringIdConverter.fs
@@ -0,0 +1,18 @@
+namespace FsCodec.SystemTextJson
+
+/// Implements conversion to/from string for a FsCodec.StringId-derived type.
+[]
+type StringIdConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
+ inherit System.Text.Json.Serialization.JsonConverter<'T>()
+ override _.Write(writer, value, _options) = value.ToString() |> writer.WriteStringValue
+ override _.Read(reader, _type, _options) = reader.GetString() |> parse
+
+/// Implements conversion to/from string for a FsCodec.StringId-derived type.
+/// Opts into use of the underlying token as a valid property name when tth type is used as a Key in a IDictionary.
+[]
+type StringIdOrDictionaryKeyConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
+ inherit System.Text.Json.Serialization.JsonConverter<'T>()
+ override _.Write(writer, value, _options) = value.ToString() |> writer.WriteStringValue
+ override _.WriteAsPropertyName(writer, value, _options) = value.ToString() |> writer.WritePropertyName
+ override _.Read(reader, _type, _options) = reader.GetString() |> parse
+ override _.ReadAsPropertyName(reader, _type, _options) = reader.GetString() |> parse
diff --git a/src/FsCodec/FsCodec.fsproj b/src/FsCodec/FsCodec.fsproj
index 1e96ea5..a7be5fa 100644
--- a/src/FsCodec/FsCodec.fsproj
+++ b/src/FsCodec/FsCodec.fsproj
@@ -11,6 +11,7 @@
+
diff --git a/src/FsCodec/StringId.fs b/src/FsCodec/StringId.fs
new file mode 100644
index 0000000..330aae6
--- /dev/null
+++ b/src/FsCodec/StringId.fs
@@ -0,0 +1,20 @@
+namespace FsCodec
+
+/// Endows any type that inherits this class with standard .NET comparison semantics using a supplied token identifier
+[]
+type Comparable<'TComp, 'Token when 'TComp :> Comparable<'TComp, 'Token> and 'Token: comparison>(token: 'Token) =
+ member private _.Token = token
+ override x.Equals y = match y with :? Comparable<'TComp, 'Token> as y -> x.Token = y.Token | _ -> false
+ override _.GetHashCode() = hash token
+ interface System.IComparable with
+ member x.CompareTo y =
+ match y with
+ | :? Comparable<'TComp, 'Token> as y -> compare x.Token y.Token
+ | _ -> invalidArg "y" "invalid comparand"
+
+/// Endows any type that inherits this class with standard .NET comparison semantics using a supplied token identifier
+/// + treats the token as the canonical rendition for `ToString()` purposes
+[]
+type StringId<'TComp when 'TComp :> Comparable<'TComp, string>>(token: string) =
+ inherit Comparable<'TComp, string>(token)
+ override _.ToString() = token
diff --git a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
index 703244f..e6e1daa 100644
--- a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
+++ b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
@@ -42,6 +42,7 @@
SomeNullHandlingTests.fs
+
diff --git a/tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs b/tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs
new file mode 100644
index 0000000..4931650
--- /dev/null
+++ b/tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs
@@ -0,0 +1,119 @@
+module FsCodec.SystemTextJson.Tests.StringIdTests
+
+open System.Collections.Generic
+open FsCodec.SystemTextJson
+open Xunit
+open Swensen.Unquote
+
+(* Recommended helper aliases to put in your namespace global to avoid having to open long namespaces *)
+
+type StjNameAttribute = System.Text.Json.Serialization.JsonPropertyNameAttribute
+type StjIgnoreAttribute = System.Text.Json.Serialization.JsonIgnoreAttribute
+type StjConverterAttribute = System.Text.Json.Serialization.JsonConverterAttribute
+
+module Guid =
+
+ let inline gen () = System.Guid.NewGuid()
+ let inline toStringN (x: System.Guid) = x.ToString "N"
+ let inline parse (x: string) = System.Guid.Parse x
+
+module Bare =
+
+ [)>]
+ type SkuId(value: System.Guid) =
+ // No JSON Ignore attribute required as read-only property
+ member val Value = value
+ and private SkuIdConverter() =
+ inherit JsonIsomorphism()
+ override _.Pickle(value: SkuId) = value.Value |> Guid.toStringN
+ override _.UnPickle input = input |> Guid.parse |> SkuId
+
+ []
+ let comparison () =
+ let g = Guid.gen ()
+ let id1, id2 = SkuId g, SkuId g
+ false =! id1.Equals id2
+ id1 <>! id2
+
+ []
+ let serdes () =
+ let x = Guid.gen () |> SkuId
+ $"\"{Guid.toStringN x.Value}\"" =! Serdes.Default.Serialize x
+ let ser = Serdes.Default.Serialize x
+ $"\"{x.Value}\"" <>! ser // Default render of Guid is not toStringN
+ x.Value =! Serdes.Default.Deserialize(ser).Value
+
+ let d = Dictionary()
+ d.Add(x, "value")
+ raises <@ Serdes.Default.Serialize d @>
+
+module StringIdIsomorphism =
+
+ [)>]
+ type SkuId(value: System.Guid) = inherit FsCodec.StringId(Guid.toStringN value)
+ and private SkuIdConverter() =
+ inherit JsonIsomorphism()
+ override _.Pickle(value: SkuId) = value |> string
+ override _.UnPickle input = input |> Guid.parse |> SkuId
+
+ []
+ let comparison () =
+ let g = Guid.gen()
+ let id1, id2 = SkuId g, SkuId g
+ true =! id1.Equals id2
+ id1 =! id2
+
+ []
+ let serdes () =
+ let x = Guid.gen () |> SkuId
+ let ser = Serdes.Default.Serialize x
+ $"\"{x}\"" =! ser
+ x =! Serdes.Default.Deserialize ser
+
+ let d = Dictionary()
+ d.Add(x, "value")
+ raises <@ Serdes.Default.Serialize d @>
+
+module StringIdConverter =
+
+ [)>]
+ type SkuId(value: System.Guid) = inherit FsCodec.StringId(Guid.toStringN value)
+ and private SkuIdConverter() = inherit StringIdConverter(Guid.parse >> SkuId)
+
+ []
+ let comparison () =
+ let g = Guid.gen()
+ let id1, id2 = SkuId g, SkuId g
+ true =! id1.Equals id2
+ id1 =! id2
+
+ []
+ let serdes () =
+ let x = Guid.gen () |> SkuId
+ $"\"{x}\"" =! Serdes.Default.Serialize x
+
+ let d = Dictionary()
+ d.Add(x, "value")
+ raises <@ Serdes.Default.Serialize d @>
+
+module StringIdOrKeyConverter =
+
+ [)>]
+ type SkuId(value: System.Guid) = inherit FsCodec.StringId(Guid.toStringN value)
+ and private SkuIdConverter() = inherit StringIdOrDictionaryKeyConverter(Guid.parse >> SkuId)
+
+ []
+ let comparison () =
+ let g = Guid.gen()
+ let id1, id2 = SkuId g, SkuId g
+ true =! id1.Equals id2
+ id1 =! id2
+
+ []
+ let serdes () =
+ let x = Guid.gen () |> SkuId
+ $"\"{x}\"" =! Serdes.Default.Serialize x
+
+ let d = Dictionary()
+ d.Add(x, "value")
+ $"{{\"{x}\":\"value\"}}" =! Serdes.Default.Serialize d