Skip to content

Commit

Permalink
Add nullable field tag to override nullability (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Sep 21, 2023
1 parent bc13a7d commit f6cad16
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ env:
GO111MODULE: "on"
CACHE_BENCHMARK: "off" # Enables benchmark result reuse between runs, may skew latency results.
RUN_BASE_BENCHMARK: "on" # Runs benchmark for PR base in case benchmark result is missing.
GO_VERSION: 1.20.x
GO_VERSION: 1.21.x
jobs:
bench:
runs-on: ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.20.x
go-version: 1.21.x
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v3.4.0
uses: golangci/golangci-lint-action@v3.7.0
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.51.1
version: v1.54.1

# Optional: working directory, useful for monorepos
# working-directory: somedir
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gorelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: 1.20.x
GO_VERSION: 1.21.x
jobs:
gorelease:
runs-on: ubuntu-latest
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ concurrency:
env:
GO111MODULE: "on"
RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing.
COV_GO_VERSION: 1.20.x # Version of Go to collect coverage
COV_GO_VERSION: 1.21.x # Version of Go to collect coverage
TARGET_DELTA_COV: 90 # Target coverage of changed lines, in percents
jobs:
test:
strategy:
matrix:
go-version: [ 1.13.x, 1.19.x, 1.20.x ]
go-version: [ 1.13.x, 1.20.x, 1.21.x ]
runs-on: ubuntu-latest
steps:
- name: Install Go stable
Expand Down Expand Up @@ -88,14 +88,14 @@ jobs:
id: annotate
if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != ''
run: |
curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.6/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.4.0/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && rm linux_amd64.tar.gz
gocovdiff_hash=$(git hash-object ./gocovdiff)
[ "$gocovdiff_hash" == "8e507e0d671d4d6dfb3612309b72b163492f28eb" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1)
[ "$gocovdiff_hash" == "f191b45548bb65ec2c7d88909679a57116ff1ba1" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1)
git fetch origin master ${{ github.event.pull_request.base.sha }}
REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV})
REP=$(./gocovdiff -mod github.com/$GITHUB_REPOSITORY -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV})
echo "${REP}"
cat gha-unit.txt
DIFF=$(test -e unit-base.txt && ./gocovdiff -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file")
DIFF=$(test -e unit-base.txt && ./gocovdiff -mod github.com/$GITHUB_REPOSITORY -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file")
TOTAL=$(cat delta-cov-unit.txt)
echo "rep<<EOF" >> $GITHUB_OUTPUT && echo "$REP" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
echo "diff<<EOF" >> $GITHUB_OUTPUT && echo "$DIFF" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ linters:
- deadcode
- testableexamples
- dupword
- depguard
- tagalign

issues:
exclude:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#GOLANGCI_LINT_VERSION := "v1.51.1" # Optional configuration to pinpoint golangci-lint version.
#GOLANGCI_LINT_VERSION := "v1.54.1" # Optional configuration to pinpoint golangci-lint version.

# The head of Makefile determines location of dev-go to include standard targets.
GO ?= go
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ These tags can be used:
* [`uniqueItems`](https://json-schema.org/draft-04/json-schema-validation.html#rfc.section.5.3.4), boolean
* [`enum`](https://json-schema.org/draft-04/json-schema-validation.html#rfc.section.5.5.1), tag value must be a JSON or comma-separated list of strings
* `required`, boolean, marks property as required
* `nullable`, boolean, overrides nullability of the property

Unnamed fields can be used to configure parent schema:

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module github.com/swaggest/jsonschema-go
go 1.18

require (
github.com/bool64/dev v0.2.29
github.com/bool64/dev v0.2.31
github.com/stretchr/testify v1.8.2
github.com/swaggest/assertjson v1.9.0
github.com/swaggest/refl v1.2.0
github.com/swaggest/refl v1.2.1
github.com/yudai/gojsondiff v1.0.0
)

Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/bool64/dev v0.2.29 h1:x+syGyh+0eWtOzQ1ItvLzOGIWyNWnyjXpHIcpF2HvL4=
github.com/bool64/dev v0.2.29/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ=
github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -35,8 +35,8 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
github.com/swaggest/refl v1.2.0 h1:Qqqhfwi7REXF6/4cwJmj3gQMzl0Q0cYquxTYdD0kvi0=
github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
github.com/swaggest/refl v1.2.1 h1:1meX9NaXjM5lmb4kk4RP3OZsXFRke9B1EHAP/pCEKO0=
github.com/swaggest/refl v1.2.1/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
Expand Down
34 changes: 34 additions & 0 deletions helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,40 @@ func (i SimpleType) ToSchemaOrBool() SchemaOrBool {
}
}

// RemoveType removes simple type from Schema.
//
// If there is no type, no change is made.
func (s *Schema) RemoveType(t SimpleType) {
if s.Type == nil {
return
}

if s.Type.SimpleTypes != nil {
if *s.Type.SimpleTypes == t {
s.Type = nil
}

return
}

if len(s.Type.SliceOfSimpleTypeValues) > 0 {
var tt []SimpleType

for _, st := range s.Type.SliceOfSimpleTypeValues {
if st != t {
tt = append(tt, st)
}
}

if len(tt) == 1 {
s.Type.SimpleTypes = &tt[0]
s.Type.SliceOfSimpleTypeValues = nil
} else {
s.Type.SliceOfSimpleTypeValues = tt
}
}
}

// AddType adds simple type to Schema.
//
// If type is already there it is ignored.
Expand Down
26 changes: 24 additions & 2 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ func checkSchemaSetup(params InterceptSchemaParams) (bool, error) {
// - `uniqueItems`, https://json-schema.org/draft-04/json-schema-validation.html#rfc.section.5.3.4
// - `enum`, tag value must be a JSON or comma-separated list of strings,
// https://json-schema.org/draft-04/json-schema-validation.html#rfc.section.5.5.1
// - `required`, boolean, marks property as required
// - `nullable`, boolean, overrides nullability of a property
//
// Unnamed fields can be used to configure parent schema:
//
Expand Down Expand Up @@ -930,6 +932,8 @@ func (r *Reflector) walkProperties(v reflect.Value, parent *Schema, rc *ReflectC
omitEmpty := strings.Contains(tag, ",omitempty")
required := false

var nullable *bool

if propName == "" {
propName = field.Name
}
Expand All @@ -938,6 +942,10 @@ func (r *Reflector) walkProperties(v reflect.Value, parent *Schema, rc *ReflectC
return err
}

if err := refl.ReadBoolPtrTag(field.Tag, "nullable", &nullable); err != nil {
return err
}

if required {
parent.Required = append(parent.Required, propName)
}
Expand Down Expand Up @@ -973,7 +981,7 @@ func (r *Reflector) walkProperties(v reflect.Value, parent *Schema, rc *ReflectC
return err
}

checkNullability(&propertySchema, rc, ft, omitEmpty)
checkNullability(&propertySchema, rc, ft, omitEmpty, nullable)

if !rc.SkipNonConstraints {
err = checkInlineValue(&propertySchema, field, "default", propertySchema.WithDefault)
Expand Down Expand Up @@ -1137,7 +1145,7 @@ func checkInlineValue(propertySchema *Schema, field reflect.StructField, tag str
// - Array, slice accepts `null` as a value.
// - Object without properties, it is a map, and it accepts `null` as a value.
// - Pointer type.
func checkNullability(propertySchema *Schema, rc *ReflectContext, ft reflect.Type, omitEmpty bool) {
func checkNullability(propertySchema *Schema, rc *ReflectContext, ft reflect.Type, omitEmpty bool, nullable *bool) {
in := InterceptNullabilityParams{
Context: rc,
OrigSchema: *propertySchema,
Expand All @@ -1152,6 +1160,20 @@ func checkNullability(propertySchema *Schema, rc *ReflectContext, ft reflect.Typ
}
}()

if nullable != nil {
if *nullable {
propertySchema.AddType(Null)

in.NullAdded = true
} else if propertySchema.Ref == nil && propertySchema.HasType(Null) {
propertySchema.RemoveType(Null)

in.NullAdded = false
}

return
}

if omitEmpty {
return
}
Expand Down
32 changes: 31 additions & 1 deletion reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ func TestReflector_Reflect_pointer_sharing(t *testing.T) {

type tt struct{}

func (t *tt) UnmarshalText(text []byte) error {
func (t *tt) UnmarshalText(_ []byte) error {
return nil
}

Expand Down Expand Up @@ -1649,3 +1649,33 @@ func TestReflector_Reflect_deeplyEmbeddedUnexported(t *testing.T) {
"type":"object"
}`, s)
}

func TestReflector_Reflect_nullable(t *testing.T) {
r := jsonschema.Reflector{}

type My struct {
List1 []string `json:"l1"`
List2 []int `json:"l2"`
List3 []string `json:"l3" nullable:"false"`
S1 string `json:"s1" nullable:"true"`
S2 *string `json:"s2" nullable:"false"`
Map1 map[string]int `json:"m1"`
Map2 map[string]int `json:"m2" nullable:"false"`
}

s, err := r.Reflect(My{})
require.NoError(t, err)

assertjson.EqMarshal(t, `{
"properties":{
"l1":{"items":{"type":"string"},"type":["array","null"]},
"l2":{"items":{"type":"integer"},"type":["array","null"]},
"l3":{"items":{"type":"string"},"type":"array"},
"m1":{"additionalProperties":{"type":"integer"},"type":["object","null"]},
"m2":{"additionalProperties":{"type":"integer"},"type":"object"},
"s1":{"type":["string","null"]},
"s2":{"type":"string"}
},
"type":"object"
}`, s)
}

0 comments on commit f6cad16

Please sign in to comment.