@@ -125,35 +125,118 @@ func ValidateContent(schema []byte, content *orderedmap.OrderedMap) error {
125125 return nil
126126}
127127
128+ // conditionalRequirement represents a field that is conditionally required based on options.dependencies.
129+ type conditionalRequirement struct {
130+ fieldName string // The field that is conditionally required
131+ dependencies map [string ]any // The dependency conditions (e.g., {"append_date": 1})
132+ }
133+
128134func NormalizeSchema (schema []byte ) ([]byte , error ) {
129135 // Decode JSON
130136 m := orderedmap .New ()
131137 if err := json .Decode (schema , & m ); err != nil {
132138 return nil , err
133139 }
134140
141+ // Collect conditional requirements per parent object path
142+ // Key is the string representation of the parent path (the object containing the properties)
143+ conditionalReqs := make (map [string ][]conditionalRequirement )
144+
135145 m .VisitAllRecursive (func (path orderedmap.Path , value any , parent any ) {
146+ lastStep := path .Last ()
147+
136148 // Required field in a JSON schema should be an array of required nested fields.
137149 // But, for historical reasons, in Keboola components, "required: true" and "required: false" are also used.
138150 // In the UI, this causes the drop-down list to not have an empty value, so the error should be ignored.
139- if path . Last () == orderedmap .MapStep ("required" ) {
151+ if lastStep == orderedmap .MapStep ("required" ) {
140152 if _ , ok := value .(bool ); ok {
141153 if parentMap , ok := parent .(* orderedmap.OrderedMap ); ok {
142154 parentMap .Delete ("required" )
143155 }
144156 }
157+ return
145158 }
146159
147160 // Empty enums are removed, we're using those for asynchronously loaded enums.
148- if path . Last () == orderedmap .MapStep ("enum" ) {
161+ if lastStep == orderedmap .MapStep ("enum" ) {
149162 if arr , ok := value .([]any ); ok && len (arr ) == 0 {
150163 if parentMap , ok := parent .(* orderedmap.OrderedMap ); ok {
151164 parentMap .Delete ("enum" )
152165 }
153166 }
167+ return
168+ }
169+
170+ // Handle options.dependencies - collect conditional requirements
171+ // Path pattern: .../properties/<fieldName>/options/dependencies
172+ if lastStep != orderedmap .MapStep ("dependencies" ) {
173+ return
174+ }
175+ pathLen := len (path )
176+ if pathLen < 4 {
177+ return
178+ }
179+ if path [pathLen - 2 ] != orderedmap .MapStep ("options" ) {
180+ return
154181 }
182+ fieldStep , ok := path [pathLen - 3 ].(orderedmap.MapStep )
183+ if ! ok {
184+ return
185+ }
186+ if path [pathLen - 4 ] != orderedmap .MapStep ("properties" ) {
187+ return
188+ }
189+ depsMap , ok := value .(* orderedmap.OrderedMap )
190+ if ! ok {
191+ return
192+ }
193+ deps := make (map [string ]any )
194+ for _ , key := range depsMap .Keys () {
195+ depValue , _ := depsMap .Get (key )
196+ deps [key ] = depValue
197+ }
198+ if len (deps ) == 0 {
199+ return
200+ }
201+ parentPath := path [:pathLen - 4 ]
202+ parentPathStr := parentPath .String ()
203+ conditionalReqs [parentPathStr ] = append (conditionalReqs [parentPathStr ], conditionalRequirement {
204+ fieldName : fieldStep .Key (),
205+ dependencies : deps ,
206+ })
155207 })
156208
209+ // Process conditional requirements: remove from required arrays and add if/then/else constructs
210+ for parentPathStr , reqs := range conditionalReqs {
211+ parentObj := getObjectAtPath (m , parentPathStr )
212+ if parentObj == nil {
213+ continue
214+ }
215+
216+ // Remove conditionally required fields from the required array
217+ removeConditionalFieldsFromRequired (parentObj , reqs )
218+
219+ // Generate if/then/else constructs for each conditional requirement
220+ allOfItems := make ([]any , 0 , len (reqs ))
221+ for _ , req := range reqs {
222+ ifThenElse := buildIfThenElse (req )
223+ if ifThenElse != nil {
224+ allOfItems = append (allOfItems , ifThenElse )
225+ }
226+ }
227+
228+ // Add allOf with if/then/else constructs to the parent object
229+ if len (allOfItems ) > 0 {
230+ // Check if allOf already exists
231+ if existingAllOf , found := parentObj .Get ("allOf" ); found {
232+ if existingArr , ok := existingAllOf .([]any ); ok {
233+ allOfItems = append (existingArr , allOfItems ... )
234+ }
235+ }
236+ parentObj .Set ("allOf" , allOfItems )
237+ }
238+ }
239+
157240 // Encode back to JSON
158241 normalized , err := json .Encode (m , false )
159242 if err != nil {
@@ -163,6 +246,100 @@ func NormalizeSchema(schema []byte) ([]byte, error) {
163246 return normalized , nil
164247}
165248
249+ // removeConditionalFieldsFromRequired removes conditionally required fields from the required array.
250+ func removeConditionalFieldsFromRequired (parentObj * orderedmap.OrderedMap , reqs []conditionalRequirement ) {
251+ requiredVal , found := parentObj .Get ("required" )
252+ if ! found {
253+ return
254+ }
255+ requiredArr , ok := requiredVal .([]any )
256+ if ! ok {
257+ return
258+ }
259+
260+ conditionalFields := make (map [string ]bool )
261+ for _ , req := range reqs {
262+ conditionalFields [req .fieldName ] = true
263+ }
264+
265+ newRequired := make ([]any , 0 , len (requiredArr ))
266+ for _ , field := range requiredArr {
267+ fieldStr , ok := field .(string )
268+ if ! ok || ! conditionalFields [fieldStr ] {
269+ newRequired = append (newRequired , field )
270+ }
271+ }
272+
273+ if len (newRequired ) > 0 {
274+ parentObj .Set ("required" , newRequired )
275+ } else {
276+ parentObj .Delete ("required" )
277+ }
278+ }
279+
280+ // getObjectAtPath returns the orderedmap at the given path string.
281+ func getObjectAtPath (m * orderedmap.OrderedMap , pathStr string ) * orderedmap.OrderedMap {
282+ if pathStr == "" {
283+ return m
284+ }
285+
286+ // Parse path string and navigate to the object
287+ // Path format: key1.key2.key3 (dot-separated)
288+ parts := strings .Split (pathStr , "." )
289+ current := m
290+ for _ , part := range parts {
291+ if part == "" {
292+ continue
293+ }
294+ val , found := current .Get (part )
295+ if ! found {
296+ return nil
297+ }
298+ nextMap , ok := val .(* orderedmap.OrderedMap )
299+ if ! ok {
300+ return nil
301+ }
302+ current = nextMap
303+ }
304+ return current
305+ }
306+
307+ // buildIfThenElse creates an if/then/else construct for a conditional requirement.
308+ func buildIfThenElse (req conditionalRequirement ) * orderedmap.OrderedMap {
309+ if len (req .dependencies ) == 0 {
310+ return nil
311+ }
312+
313+ // Build the "if" condition properties
314+ ifProperties := orderedmap .New ()
315+ for depField , depValue := range req .dependencies {
316+ condition := orderedmap .New ()
317+ // Handle array values (e.g., "protocol": ["FTP", "FTPS"]) using enum
318+ // Handle single values using const
319+ if arr , ok := depValue .([]any ); ok {
320+ condition .Set ("enum" , arr )
321+ } else {
322+ condition .Set ("const" , depValue )
323+ }
324+ ifProperties .Set (depField , condition )
325+ }
326+
327+ // Build the "if" clause
328+ ifClause := orderedmap .New ()
329+ ifClause .Set ("properties" , ifProperties )
330+
331+ // Build the "then" clause with required field
332+ thenClause := orderedmap .New ()
333+ thenClause .Set ("required" , []any {req .fieldName })
334+
335+ // Build the complete if/then construct
336+ result := orderedmap .New ()
337+ result .Set ("if" , ifClause )
338+ result .Set ("then" , thenClause )
339+
340+ return result
341+ }
342+
166343func validateDocument (schemaStr []byte , document * orderedmap.OrderedMap ) error {
167344 schema , err := compileSchema (schemaStr , false )
168345 if err != nil {
0 commit comments